commit
01171a1ffb
699 changed files with 83062 additions and 0 deletions
-
47.gitignore
-
20LICENSE
-
95README.md
-
12bin/clean.bat
-
12bin/package.bat
-
14bin/run.bat
-
125chenhai-admin/pom.xml
-
30chenhai-admin/src/main/java/com/chenhai/RuoYiApplication.java
-
18chenhai-admin/src/main/java/com/chenhai/RuoYiServletInitializer.java
-
94chenhai-admin/src/main/java/com/chenhai/web/controller/common/CaptchaController.java
-
162chenhai-admin/src/main/java/com/chenhai/web/controller/common/CommonController.java
-
122chenhai-admin/src/main/java/com/chenhai/web/controller/monitor/CacheController.java
-
27chenhai-admin/src/main/java/com/chenhai/web/controller/monitor/ServerController.java
-
82chenhai-admin/src/main/java/com/chenhai/web/controller/monitor/SysLogininforController.java
-
69chenhai-admin/src/main/java/com/chenhai/web/controller/monitor/SysOperlogController.java
-
83chenhai-admin/src/main/java/com/chenhai/web/controller/monitor/SysUserOnlineController.java
-
133chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysConfigController.java
-
132chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysDeptController.java
-
121chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysDictDataController.java
-
131chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysDictTypeController.java
-
29chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysIndexController.java
-
131chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysLoginController.java
-
142chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysMenuController.java
-
91chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysNoticeController.java
-
129chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysPostController.java
-
148chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysProfileController.java
-
38chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysRegisterController.java
-
262chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysRoleController.java
-
256chenhai-admin/src/main/java/com/chenhai/web/controller/system/SysUserController.java
-
175chenhai-admin/src/main/java/com/chenhai/web/controller/tool/TestController.java
-
64chenhai-admin/src/main/java/com/chenhai/web/core/config/SwaggerConfig.java
-
1chenhai-admin/src/main/resources/META-INF/spring-devtools.properties
-
108chenhai-admin/src/main/resources/application-druid.yml
-
148chenhai-admin/src/main/resources/application.yml
-
24chenhai-admin/src/main/resources/banner.txt
-
38chenhai-admin/src/main/resources/i18n/messages.properties
-
93chenhai-admin/src/main/resources/logback.xml
-
20chenhai-admin/src/main/resources/mybatis/mybatis-config.xml
-
78chenhai-ai/pom.xml
-
43chenhai-ai/src/main/java/com/chenhai/chenhaiai/config/AsyncConfig.java
-
28chenhai-ai/src/main/java/com/chenhai/chenhaiai/config/BeanChecker.java
-
38chenhai-ai/src/main/java/com/chenhai/chenhaiai/config/ChatClientConfig.java
-
141chenhai-ai/src/main/java/com/chenhai/chenhaiai/config/ChatModelFactory.java
-
152chenhai-ai/src/main/java/com/chenhai/chenhaiai/config/GraphConfig.java
-
18chenhai-ai/src/main/java/com/chenhai/chenhaiai/config/ProgressEmitterConfig.java
-
84chenhai-ai/src/main/java/com/chenhai/chenhaiai/controller/GiteaController.java
-
526chenhai-ai/src/main/java/com/chenhai/chenhaiai/controller/GraphController.java
-
146chenhai-ai/src/main/java/com/chenhai/chenhaiai/controller/McpController.java
-
125chenhai-ai/src/main/java/com/chenhai/chenhaiai/controller/ModelController.java
-
40chenhai-ai/src/main/java/com/chenhai/chenhaiai/controller/RealTestController.java
-
109chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/AnalysisResult.java
-
21chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/DailyPaper.java
-
22chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/Dept.java
-
16chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/UserInfo.java
-
21chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/WeekPlanDetail.java
-
21chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/WeekPlanMain.java
-
58chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/WeekPlanResponse.java
-
37chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/WeekProject.java
-
22chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/BasicInfo.java
-
13chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/DayStats.java
-
94chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/DeveloperActivity.java
-
15chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/DeveloperRank.java
-
13chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/FileTypeStats.java
-
23chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/GitAnalysisData.java
-
203chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/GiteaCommit.java
-
116chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/GiteaRepository.java
-
16chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/RepoRank.java
-
98chenhai-ai/src/main/java/com/chenhai/chenhaiai/entity/git/RepositoryActivity.java
-
135chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/DataAssociationNode.java
-
83chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/DataOrganizationNode.java
-
167chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/DataTranslationNode.java
-
83chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/WeekPlanAnalysisNode.java
-
139chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/Analysis.java
-
148chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/AnalysisStreamNode.java
-
69chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/jdbc/DailyPaperJdbcNode.java
-
68chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/jdbc/DeptJdbcNode.java
-
128chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/jdbc/GitAnalysisNode.java
-
66chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/jdbc/UserJdbcNode.java
-
67chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/jdbc/WeekPlanDetailJdbcNode.java
-
75chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/jdbc/WeekPlanMainJdbcNode.java
-
68chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/mcp/DailyPaperNode.java
-
51chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/mcp/DeptNode.java
-
50chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/mcp/UserNode.java
-
50chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/mcp/WeekPlanDetailNode.java
-
51chenhai-ai/src/main/java/com/chenhai/chenhaiai/node/weekPlan/mcp/WeekPlanMainNode.java
-
117chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/AnalysisStreamService.java
-
583chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/GiteaAnalysisParallelService.java
-
50chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/GiteaAnalysisParallelTest.java
-
570chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/GiteaAnalysisService.java
-
398chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/GiteaAnalysisTest.java
-
605chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/LongTermGiteaAnalysisService.java
-
208chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/MarkdownService.java
-
172chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/SimpleConcurrentTest.java
-
907chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/gitNew/GiteaDataService.java
-
946chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/gitNew/GiteaGranularityService.java
-
802chenhai-ai/src/main/java/com/chenhai/chenhaiai/service/gitNew/GiteaQueryService.java
-
209chenhai-ai/src/main/java/com/chenhai/chenhaiai/utils/CharacterStreamProcessor.java
-
24chenhai-ai/src/main/java/com/chenhai/chenhaiai/utils/ProgressEmitter.java
-
277chenhai-ai/src/main/java/com/chenhai/chenhaiai/utils/PromptLoader.java
-
81chenhai-ai/src/main/java/com/chenhai/chenhaiai/utils/TextFormatUtils.java
@ -0,0 +1,47 @@ |
|||||
|
###################################################################### |
||||
|
# Build Tools |
||||
|
|
||||
|
.gradle |
||||
|
/build/ |
||||
|
!gradle/wrapper/gradle-wrapper.jar |
||||
|
|
||||
|
target/ |
||||
|
!.mvn/wrapper/maven-wrapper.jar |
||||
|
|
||||
|
###################################################################### |
||||
|
# IDE |
||||
|
|
||||
|
### STS ### |
||||
|
.apt_generated |
||||
|
.classpath |
||||
|
.factorypath |
||||
|
.project |
||||
|
.settings |
||||
|
.springBeans |
||||
|
|
||||
|
### IntelliJ IDEA ### |
||||
|
.idea |
||||
|
*.iws |
||||
|
*.iml |
||||
|
*.ipr |
||||
|
|
||||
|
### JRebel ### |
||||
|
rebel.xml |
||||
|
|
||||
|
### NetBeans ### |
||||
|
nbproject/private/ |
||||
|
build/* |
||||
|
nbbuild/ |
||||
|
dist/ |
||||
|
nbdist/ |
||||
|
.nb-gradle/ |
||||
|
|
||||
|
###################################################################### |
||||
|
# Others |
||||
|
*.log |
||||
|
*.xml.versionsBackup |
||||
|
*.swp |
||||
|
|
||||
|
!*/build/*.java |
||||
|
!*/build/*.html |
||||
|
!*/build/*.xml |
||||
@ -0,0 +1,20 @@ |
|||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2018 RuoYi |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
|
this software and associated documentation files (the "Software"), to deal in |
||||
|
the Software without restriction, including without limitation the rights to |
||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||
|
the Software, and to permit persons to whom the Software is furnished to do so, |
||||
|
subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
@ -0,0 +1,95 @@ |
|||||
|
<p align="center"> |
||||
|
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png"> |
||||
|
</p> |
||||
|
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.0</h1> |
||||
|
<h4 align="center">基于SpringBoot+Vue前后端分离的Java快速开发框架</h4> |
||||
|
<p align="center"> |
||||
|
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a> |
||||
|
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.0-brightgreen.svg"></a> |
||||
|
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a> |
||||
|
</p> |
||||
|
|
||||
|
## 平台简介 |
||||
|
|
||||
|
若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 |
||||
|
|
||||
|
* 前端采用Vue、Element UI。 |
||||
|
* 后端采用Spring Boot、Spring Security、Redis & Jwt。 |
||||
|
* 权限认证使用Jwt,支持多终端认证系统。 |
||||
|
* 支持加载动态权限菜单,多方式轻松权限控制。 |
||||
|
* 高效率开发,使用代码生成器可以一键生成前后端代码。 |
||||
|
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3),保持同步更新。 |
||||
|
* 提供了单应用版本[RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast),Oracle版本[RuoYi-Vue-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle),保持同步更新。 |
||||
|
* 不分离版本,请移步[RuoYi](https://gitee.com/y_project/RuoYi),微服务版本,请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud) |
||||
|
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip) |
||||
|
|
||||
|
## 内置功能 |
||||
|
|
||||
|
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 |
||||
|
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 |
||||
|
3. 岗位管理:配置系统用户所属担任职务。 |
||||
|
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 |
||||
|
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 |
||||
|
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 |
||||
|
7. 参数管理:对系统动态配置常用参数。 |
||||
|
8. 通知公告:系统通知公告信息发布维护。 |
||||
|
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 |
||||
|
10. 登录日志:系统登录日志记录查询包含登录异常。 |
||||
|
11. 在线用户:当前系统中活跃用户状态监控。 |
||||
|
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。 |
||||
|
13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。 |
||||
|
14. 系统接口:根据业务代码自动生成相关的api接口文档。 |
||||
|
15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。 |
||||
|
16. 缓存监控:对系统的缓存信息查询,命令统计等。 |
||||
|
17. 在线构建器:拖动表单元素生成相应的HTML代码。 |
||||
|
18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。 |
||||
|
|
||||
|
## 在线体验 |
||||
|
|
||||
|
- admin/admin123 |
||||
|
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。 |
||||
|
|
||||
|
演示地址:http://vue.ruoyi.vip |
||||
|
文档地址:http://doc.ruoyi.vip |
||||
|
|
||||
|
## 演示图 |
||||
|
|
||||
|
<table> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td> |
||||
|
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
|
||||
|
|
||||
|
## 若依前后端分离交流群 |
||||
|
|
||||
|
QQ群: [](https://jq.qq.com/?_wv=1027&k=5bVB1og) [](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [](https://jq.qq.com/?_wv=1027&k=51G72yr) [](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [](https://jq.qq.com/?_wv=1027&k=SpyH2875) [](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) 点击按钮入群。 |
||||
@ -0,0 +1,12 @@ |
|||||
|
@echo off |
||||
|
echo. |
||||
|
echo [信息] 清理工程target生成路径。 |
||||
|
echo. |
||||
|
|
||||
|
%~d0 |
||||
|
cd %~dp0 |
||||
|
|
||||
|
cd .. |
||||
|
call mvn clean |
||||
|
|
||||
|
pause |
||||
@ -0,0 +1,12 @@ |
|||||
|
@echo off |
||||
|
echo. |
||||
|
echo [信息] 打包Web工程,生成war/jar包文件。 |
||||
|
echo. |
||||
|
|
||||
|
%~d0 |
||||
|
cd %~dp0 |
||||
|
|
||||
|
cd .. |
||||
|
call mvn clean package -Dmaven.test.skip=true |
||||
|
|
||||
|
pause |
||||
@ -0,0 +1,14 @@ |
|||||
|
@echo off |
||||
|
echo. |
||||
|
echo [信息] 使用Jar命令运行Web工程。 |
||||
|
echo. |
||||
|
|
||||
|
cd %~dp0 |
||||
|
cd ../chenhai-admin/target |
||||
|
|
||||
|
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m |
||||
|
|
||||
|
java -jar %JAVA_OPTS% chenhai-admin.jar |
||||
|
|
||||
|
cd bin |
||||
|
pause |
||||
@ -0,0 +1,125 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
|
<parent> |
||||
|
<artifactId>chenhai</artifactId> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<version>3.9.0</version> |
||||
|
</parent> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
<packaging>jar</packaging> |
||||
|
<artifactId>chenhai-admin</artifactId> |
||||
|
|
||||
|
<description> |
||||
|
web服务入口 |
||||
|
</description> |
||||
|
|
||||
|
<dependencies> |
||||
|
|
||||
|
<!-- spring-boot-devtools --> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-devtools</artifactId> |
||||
|
<optional>true</optional> <!-- 表示依赖不会传递 --> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- spring-doc --> |
||||
|
<dependency> |
||||
|
<groupId>org.springdoc</groupId> |
||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Mysql驱动包 --> |
||||
|
<dependency> |
||||
|
<groupId>com.mysql</groupId> |
||||
|
<artifactId>mysql-connector-j</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 核心模块--> |
||||
|
<dependency> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<artifactId>chenhai-framework</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 定时任务--> |
||||
|
<dependency> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<artifactId>chenhai-quartz</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 代码生成--> |
||||
|
<dependency> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<artifactId>chenhai-generator</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- ai--> |
||||
|
<dependency> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<artifactId>chenhai-ai</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
</dependencies> |
||||
|
|
||||
|
<build> |
||||
|
<plugins> |
||||
|
<plugin> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-maven-plugin</artifactId> |
||||
|
<version>3.5.4</version> |
||||
|
<configuration> |
||||
|
<addResources>true</addResources> |
||||
|
</configuration> |
||||
|
<executions> |
||||
|
<execution> |
||||
|
<goals> |
||||
|
<goal>repackage</goal> |
||||
|
</goals> |
||||
|
</execution> |
||||
|
</executions> |
||||
|
</plugin> |
||||
|
|
||||
|
<!-- ==================== 我添加的配置开始 ==================== --> |
||||
|
<!-- 资源解压插件:将chenhai-ai模块的提示词文件解压到当前模块 --> |
||||
|
<plugin> |
||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||
|
<artifactId>maven-dependency-plugin</artifactId> |
||||
|
<executions> |
||||
|
<execution> |
||||
|
<id>unpack-ai-resources</id> |
||||
|
<phase>generate-resources</phase> |
||||
|
<goals> |
||||
|
<goal>unpack</goal> |
||||
|
</goals> |
||||
|
<configuration> |
||||
|
<artifactItems> |
||||
|
<artifactItem> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<artifactId>chenhai-ai</artifactId> |
||||
|
<type>jar</type> |
||||
|
<overWrite>true</overWrite> |
||||
|
<outputDirectory>${project.build.outputDirectory}</outputDirectory> |
||||
|
<includes>**/prompts/**</includes> |
||||
|
</artifactItem> |
||||
|
</artifactItems> |
||||
|
</configuration> |
||||
|
</execution> |
||||
|
</executions> |
||||
|
</plugin> |
||||
|
<!-- ==================== 我添加的配置结束 ==================== --> |
||||
|
|
||||
|
<plugin> |
||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||
|
<artifactId>maven-war-plugin</artifactId> |
||||
|
<version>3.1.0</version> |
||||
|
<configuration> |
||||
|
<failOnMissingWebXml>false</failOnMissingWebXml> |
||||
|
<warName>${project.artifactId}</warName> |
||||
|
</configuration> |
||||
|
</plugin> |
||||
|
</plugins> |
||||
|
<finalName>${project.artifactId}</finalName> |
||||
|
</build> |
||||
|
|
||||
|
</project> |
||||
@ -0,0 +1,30 @@ |
|||||
|
package com.chenhai; |
||||
|
|
||||
|
import org.springframework.boot.SpringApplication; |
||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; |
||||
|
|
||||
|
/** |
||||
|
* 启动程序 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) |
||||
|
public class RuoYiApplication |
||||
|
{ |
||||
|
public static void main(String[] args) |
||||
|
{ |
||||
|
// System.setProperty("spring.devtools.restart.enabled", "false"); |
||||
|
SpringApplication.run(RuoYiApplication.class, args); |
||||
|
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" + |
||||
|
" .-------. ____ __ \n" + |
||||
|
" | _ _ \\ \\ \\ / / \n" + |
||||
|
" | ( ' ) | \\ _. / ' \n" + |
||||
|
" |(_ o _) / _( )_ .' \n" + |
||||
|
" | (_,_).' __ ___(_ o _)' \n" + |
||||
|
" | |\\ \\ | || |(_,_)' \n" + |
||||
|
" | | \\ `' /| `-' / \n" + |
||||
|
" | | \\ / \\ / \n" + |
||||
|
" ''-' `'-' `-..-' "); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
package com.chenhai; |
||||
|
|
||||
|
import org.springframework.boot.builder.SpringApplicationBuilder; |
||||
|
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; |
||||
|
|
||||
|
/** |
||||
|
* web容器中进行部署 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
public class RuoYiServletInitializer extends SpringBootServletInitializer |
||||
|
{ |
||||
|
@Override |
||||
|
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) |
||||
|
{ |
||||
|
return application.sources(RuoYiApplication.class); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
package com.chenhai.web.controller.common; |
||||
|
|
||||
|
import java.awt.image.BufferedImage; |
||||
|
import java.io.IOException; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import jakarta.annotation.Resource; |
||||
|
import javax.imageio.ImageIO; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.util.FastByteArrayOutputStream; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.google.code.kaptcha.Producer; |
||||
|
import com.chenhai.common.config.RuoYiConfig; |
||||
|
import com.chenhai.common.constant.CacheConstants; |
||||
|
import com.chenhai.common.constant.Constants; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.redis.RedisCache; |
||||
|
import com.chenhai.common.utils.sign.Base64; |
||||
|
import com.chenhai.common.utils.uuid.IdUtils; |
||||
|
import com.chenhai.system.service.ISysConfigService; |
||||
|
|
||||
|
/** |
||||
|
* 验证码操作处理 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
public class CaptchaController |
||||
|
{ |
||||
|
@Resource(name = "captchaProducer") |
||||
|
private Producer captchaProducer; |
||||
|
|
||||
|
@Resource(name = "captchaProducerMath") |
||||
|
private Producer captchaProducerMath; |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisCache redisCache; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysConfigService configService; |
||||
|
/** |
||||
|
* 生成验证码 |
||||
|
*/ |
||||
|
@GetMapping("/captchaImage") |
||||
|
public AjaxResult getCode(HttpServletResponse response) throws IOException |
||||
|
{ |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
boolean captchaEnabled = configService.selectCaptchaEnabled(); |
||||
|
ajax.put("captchaEnabled", captchaEnabled); |
||||
|
if (!captchaEnabled) |
||||
|
{ |
||||
|
return ajax; |
||||
|
} |
||||
|
|
||||
|
// 保存验证码信息 |
||||
|
String uuid = IdUtils.simpleUUID(); |
||||
|
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; |
||||
|
|
||||
|
String capStr = null, code = null; |
||||
|
BufferedImage image = null; |
||||
|
|
||||
|
// 生成验证码 |
||||
|
String captchaType = RuoYiConfig.getCaptchaType(); |
||||
|
if ("math".equals(captchaType)) |
||||
|
{ |
||||
|
String capText = captchaProducerMath.createText(); |
||||
|
capStr = capText.substring(0, capText.lastIndexOf("@")); |
||||
|
code = capText.substring(capText.lastIndexOf("@") + 1); |
||||
|
image = captchaProducerMath.createImage(capStr); |
||||
|
} |
||||
|
else if ("char".equals(captchaType)) |
||||
|
{ |
||||
|
capStr = code = captchaProducer.createText(); |
||||
|
image = captchaProducer.createImage(capStr); |
||||
|
} |
||||
|
|
||||
|
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); |
||||
|
// 转换流信息写出 |
||||
|
FastByteArrayOutputStream os = new FastByteArrayOutputStream(); |
||||
|
try |
||||
|
{ |
||||
|
ImageIO.write(image, "jpg", os); |
||||
|
} |
||||
|
catch (IOException e) |
||||
|
{ |
||||
|
return AjaxResult.error(e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
ajax.put("uuid", uuid); |
||||
|
ajax.put("img", Base64.encode(os.toByteArray())); |
||||
|
return ajax; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,162 @@ |
|||||
|
package com.chenhai.web.controller.common; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.http.MediaType; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
import com.chenhai.common.config.RuoYiConfig; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.common.utils.file.FileUploadUtils; |
||||
|
import com.chenhai.common.utils.file.FileUtils; |
||||
|
import com.chenhai.framework.config.ServerConfig; |
||||
|
|
||||
|
/** |
||||
|
* 通用请求处理 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/common") |
||||
|
public class CommonController |
||||
|
{ |
||||
|
private static final Logger log = LoggerFactory.getLogger(CommonController.class); |
||||
|
|
||||
|
@Autowired |
||||
|
private ServerConfig serverConfig; |
||||
|
|
||||
|
private static final String FILE_DELIMETER = ","; |
||||
|
|
||||
|
/** |
||||
|
* 通用下载请求 |
||||
|
* |
||||
|
* @param fileName 文件名称 |
||||
|
* @param delete 是否删除 |
||||
|
*/ |
||||
|
@GetMapping("/download") |
||||
|
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (!FileUtils.checkAllowDownload(fileName)) |
||||
|
{ |
||||
|
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); |
||||
|
} |
||||
|
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); |
||||
|
String filePath = RuoYiConfig.getDownloadPath() + fileName; |
||||
|
|
||||
|
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); |
||||
|
FileUtils.setAttachmentResponseHeader(response, realFileName); |
||||
|
FileUtils.writeBytes(filePath, response.getOutputStream()); |
||||
|
if (delete) |
||||
|
{ |
||||
|
FileUtils.deleteFile(filePath); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
log.error("下载文件失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 通用上传请求(单个) |
||||
|
*/ |
||||
|
@PostMapping("/upload") |
||||
|
public AjaxResult uploadFile(MultipartFile file) throws Exception |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
// 上传文件路径 |
||||
|
String filePath = RuoYiConfig.getUploadPath(); |
||||
|
// 上传并返回新文件名称 |
||||
|
String fileName = FileUploadUtils.upload(filePath, file); |
||||
|
String url = serverConfig.getUrl() + fileName; |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
ajax.put("url", url); |
||||
|
ajax.put("fileName", fileName); |
||||
|
ajax.put("newFileName", FileUtils.getName(fileName)); |
||||
|
ajax.put("originalFilename", file.getOriginalFilename()); |
||||
|
return ajax; |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
return AjaxResult.error(e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 通用上传请求(多个) |
||||
|
*/ |
||||
|
@PostMapping("/uploads") |
||||
|
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
// 上传文件路径 |
||||
|
String filePath = RuoYiConfig.getUploadPath(); |
||||
|
List<String> urls = new ArrayList<String>(); |
||||
|
List<String> fileNames = new ArrayList<String>(); |
||||
|
List<String> newFileNames = new ArrayList<String>(); |
||||
|
List<String> originalFilenames = new ArrayList<String>(); |
||||
|
for (MultipartFile file : files) |
||||
|
{ |
||||
|
// 上传并返回新文件名称 |
||||
|
String fileName = FileUploadUtils.upload(filePath, file); |
||||
|
String url = serverConfig.getUrl() + fileName; |
||||
|
urls.add(url); |
||||
|
fileNames.add(fileName); |
||||
|
newFileNames.add(FileUtils.getName(fileName)); |
||||
|
originalFilenames.add(file.getOriginalFilename()); |
||||
|
} |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); |
||||
|
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); |
||||
|
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER)); |
||||
|
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER)); |
||||
|
return ajax; |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
return AjaxResult.error(e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 本地资源通用下载 |
||||
|
*/ |
||||
|
@GetMapping("/download/resource") |
||||
|
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) |
||||
|
throws Exception |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (!FileUtils.checkAllowDownload(resource)) |
||||
|
{ |
||||
|
throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); |
||||
|
} |
||||
|
// 本地资源路径 |
||||
|
String localPath = RuoYiConfig.getProfile(); |
||||
|
// 数据库资源地址 |
||||
|
String downloadPath = localPath + FileUtils.stripPrefix(resource); |
||||
|
// 下载名称 |
||||
|
String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); |
||||
|
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); |
||||
|
FileUtils.setAttachmentResponseHeader(response, downloadName); |
||||
|
FileUtils.writeBytes(downloadPath, response.getOutputStream()); |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
log.error("下载文件失败", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
package com.chenhai.web.controller.monitor; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Collection; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Properties; |
||||
|
import java.util.Set; |
||||
|
import java.util.TreeSet; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.data.redis.core.RedisCallback; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.constant.CacheConstants; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.system.domain.SysCache; |
||||
|
|
||||
|
/** |
||||
|
* 缓存监控 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/monitor/cache") |
||||
|
public class CacheController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private RedisTemplate<String, String> redisTemplate; |
||||
|
|
||||
|
private final static List<SysCache> caches = new ArrayList<SysCache>(); |
||||
|
{ |
||||
|
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息")); |
||||
|
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息")); |
||||
|
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典")); |
||||
|
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码")); |
||||
|
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交")); |
||||
|
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); |
||||
|
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); |
||||
|
} |
||||
|
|
||||
|
@SuppressWarnings("deprecation") |
||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") |
||||
|
@GetMapping() |
||||
|
public AjaxResult getInfo() throws Exception |
||||
|
{ |
||||
|
Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info()); |
||||
|
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats")); |
||||
|
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize()); |
||||
|
|
||||
|
Map<String, Object> result = new HashMap<>(3); |
||||
|
result.put("info", info); |
||||
|
result.put("dbSize", dbSize); |
||||
|
|
||||
|
List<Map<String, String>> pieList = new ArrayList<>(); |
||||
|
commandStats.stringPropertyNames().forEach(key -> { |
||||
|
Map<String, String> data = new HashMap<>(2); |
||||
|
String property = commandStats.getProperty(key); |
||||
|
data.put("name", StringUtils.removeStart(key, "cmdstat_")); |
||||
|
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec")); |
||||
|
pieList.add(data); |
||||
|
}); |
||||
|
result.put("commandStats", pieList); |
||||
|
return AjaxResult.success(result); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") |
||||
|
@GetMapping("/getNames") |
||||
|
public AjaxResult cache() |
||||
|
{ |
||||
|
return AjaxResult.success(caches); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") |
||||
|
@GetMapping("/getKeys/{cacheName}") |
||||
|
public AjaxResult getCacheKeys(@PathVariable String cacheName) |
||||
|
{ |
||||
|
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*"); |
||||
|
return AjaxResult.success(new TreeSet<>(cacheKeys)); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") |
||||
|
@GetMapping("/getValue/{cacheName}/{cacheKey}") |
||||
|
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) |
||||
|
{ |
||||
|
String cacheValue = redisTemplate.opsForValue().get(cacheKey); |
||||
|
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue); |
||||
|
return AjaxResult.success(sysCache); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") |
||||
|
@DeleteMapping("/clearCacheName/{cacheName}") |
||||
|
public AjaxResult clearCacheName(@PathVariable String cacheName) |
||||
|
{ |
||||
|
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*"); |
||||
|
redisTemplate.delete(cacheKeys); |
||||
|
return AjaxResult.success(); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") |
||||
|
@DeleteMapping("/clearCacheKey/{cacheKey}") |
||||
|
public AjaxResult clearCacheKey(@PathVariable String cacheKey) |
||||
|
{ |
||||
|
redisTemplate.delete(cacheKey); |
||||
|
return AjaxResult.success(); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") |
||||
|
@DeleteMapping("/clearCacheAll") |
||||
|
public AjaxResult clearCacheAll() |
||||
|
{ |
||||
|
Collection<String> cacheKeys = redisTemplate.keys("*"); |
||||
|
redisTemplate.delete(cacheKeys); |
||||
|
return AjaxResult.success(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
package com.chenhai.web.controller.monitor; |
||||
|
|
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.framework.web.domain.Server; |
||||
|
|
||||
|
/** |
||||
|
* 服务器监控 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/monitor/server") |
||||
|
public class ServerController |
||||
|
{ |
||||
|
@PreAuthorize("@ss.hasPermi('monitor:server:list')") |
||||
|
@GetMapping() |
||||
|
public AjaxResult getInfo() throws Exception |
||||
|
{ |
||||
|
Server server = new Server(); |
||||
|
server.copyTo(); |
||||
|
return AjaxResult.success(server); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
package com.chenhai.web.controller.monitor; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.framework.web.service.SysPasswordService; |
||||
|
import com.chenhai.system.domain.SysLogininfor; |
||||
|
import com.chenhai.system.service.ISysLogininforService; |
||||
|
|
||||
|
/** |
||||
|
* 系统访问记录 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/monitor/logininfor") |
||||
|
public class SysLogininforController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysLogininforService logininforService; |
||||
|
|
||||
|
@Autowired |
||||
|
private SysPasswordService passwordService; |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysLogininfor logininfor) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "登录日志", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('monitor:logininfor:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysLogininfor logininfor) |
||||
|
{ |
||||
|
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor); |
||||
|
ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class); |
||||
|
util.exportExcel(response, list, "登录日志"); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") |
||||
|
@Log(title = "登录日志", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{infoIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] infoIds) |
||||
|
{ |
||||
|
return toAjax(logininforService.deleteLogininforByIds(infoIds)); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") |
||||
|
@Log(title = "登录日志", businessType = BusinessType.CLEAN) |
||||
|
@DeleteMapping("/clean") |
||||
|
public AjaxResult clean() |
||||
|
{ |
||||
|
logininforService.cleanLogininfor(); |
||||
|
return success(); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')") |
||||
|
@Log(title = "账户解锁", businessType = BusinessType.OTHER) |
||||
|
@GetMapping("/unlock/{userName}") |
||||
|
public AjaxResult unlock(@PathVariable("userName") String userName) |
||||
|
{ |
||||
|
passwordService.clearLoginRecordCache(userName); |
||||
|
return success(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
package com.chenhai.web.controller.monitor; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.system.domain.SysOperLog; |
||||
|
import com.chenhai.system.service.ISysOperLogService; |
||||
|
|
||||
|
/** |
||||
|
* 操作日志记录 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/monitor/operlog") |
||||
|
public class SysOperlogController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysOperLogService operLogService; |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:operlog:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysOperLog operLog) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysOperLog> list = operLogService.selectOperLogList(operLog); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "操作日志", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('monitor:operlog:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysOperLog operLog) |
||||
|
{ |
||||
|
List<SysOperLog> list = operLogService.selectOperLogList(operLog); |
||||
|
ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class); |
||||
|
util.exportExcel(response, list, "操作日志"); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "操作日志", businessType = BusinessType.DELETE) |
||||
|
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") |
||||
|
@DeleteMapping("/{operIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] operIds) |
||||
|
{ |
||||
|
return toAjax(operLogService.deleteOperLogByIds(operIds)); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "操作日志", businessType = BusinessType.CLEAN) |
||||
|
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") |
||||
|
@DeleteMapping("/clean") |
||||
|
public AjaxResult clean() |
||||
|
{ |
||||
|
operLogService.cleanOperLog(); |
||||
|
return success(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
package com.chenhai.web.controller.monitor; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Collection; |
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.constant.CacheConstants; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.model.LoginUser; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.core.redis.RedisCache; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.system.domain.SysUserOnline; |
||||
|
import com.chenhai.system.service.ISysUserOnlineService; |
||||
|
|
||||
|
/** |
||||
|
* 在线用户监控 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/monitor/online") |
||||
|
public class SysUserOnlineController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysUserOnlineService userOnlineService; |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisCache redisCache; |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('monitor:online:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(String ipaddr, String userName) |
||||
|
{ |
||||
|
Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); |
||||
|
List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>(); |
||||
|
for (String key : keys) |
||||
|
{ |
||||
|
LoginUser user = redisCache.getCacheObject(key); |
||||
|
if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) |
||||
|
{ |
||||
|
userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); |
||||
|
} |
||||
|
else if (StringUtils.isNotEmpty(ipaddr)) |
||||
|
{ |
||||
|
userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); |
||||
|
} |
||||
|
else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) |
||||
|
{ |
||||
|
userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); |
||||
|
} |
||||
|
} |
||||
|
Collections.reverse(userOnlineList); |
||||
|
userOnlineList.removeAll(Collections.singleton(null)); |
||||
|
return getDataTable(userOnlineList); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 强退用户 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')") |
||||
|
@Log(title = "在线用户", businessType = BusinessType.FORCE) |
||||
|
@DeleteMapping("/{tokenId}") |
||||
|
public AjaxResult forceLogout(@PathVariable String tokenId) |
||||
|
{ |
||||
|
redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId); |
||||
|
return success(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,133 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.system.domain.SysConfig; |
||||
|
import com.chenhai.system.service.ISysConfigService; |
||||
|
|
||||
|
/** |
||||
|
* 参数配置 信息操作处理 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/config") |
||||
|
public class SysConfigController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysConfigService configService; |
||||
|
|
||||
|
/** |
||||
|
* 获取参数配置列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:config:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysConfig config) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysConfig> list = configService.selectConfigList(config); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "参数管理", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('system:config:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysConfig config) |
||||
|
{ |
||||
|
List<SysConfig> list = configService.selectConfigList(config); |
||||
|
ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class); |
||||
|
util.exportExcel(response, list, "参数数据"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据参数编号获取详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:config:query')") |
||||
|
@GetMapping(value = "/{configId}") |
||||
|
public AjaxResult getInfo(@PathVariable Long configId) |
||||
|
{ |
||||
|
return success(configService.selectConfigById(configId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据参数键名查询参数值 |
||||
|
*/ |
||||
|
@GetMapping(value = "/configKey/{configKey}") |
||||
|
public AjaxResult getConfigKey(@PathVariable String configKey) |
||||
|
{ |
||||
|
return success(configService.selectConfigByKey(configKey)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增参数配置 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:config:add')") |
||||
|
@Log(title = "参数管理", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysConfig config) |
||||
|
{ |
||||
|
if (!configService.checkConfigKeyUnique(config)) |
||||
|
{ |
||||
|
return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); |
||||
|
} |
||||
|
config.setCreateBy(getUsername()); |
||||
|
return toAjax(configService.insertConfig(config)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改参数配置 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:config:edit')") |
||||
|
@Log(title = "参数管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysConfig config) |
||||
|
{ |
||||
|
if (!configService.checkConfigKeyUnique(config)) |
||||
|
{ |
||||
|
return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); |
||||
|
} |
||||
|
config.setUpdateBy(getUsername()); |
||||
|
return toAjax(configService.updateConfig(config)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除参数配置 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:config:remove')") |
||||
|
@Log(title = "参数管理", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{configIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] configIds) |
||||
|
{ |
||||
|
configService.deleteConfigByIds(configIds); |
||||
|
return success(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 刷新参数缓存 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:config:remove')") |
||||
|
@Log(title = "参数管理", businessType = BusinessType.CLEAN) |
||||
|
@DeleteMapping("/refreshCache") |
||||
|
public AjaxResult refreshCache() |
||||
|
{ |
||||
|
configService.resetConfigCache(); |
||||
|
return success(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import org.apache.commons.lang3.ArrayUtils; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.constant.UserConstants; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysDept; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.system.service.ISysDeptService; |
||||
|
|
||||
|
/** |
||||
|
* 部门信息 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/dept") |
||||
|
public class SysDeptController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysDeptService deptService; |
||||
|
|
||||
|
/** |
||||
|
* 获取部门列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dept:list')") |
||||
|
@GetMapping("/list") |
||||
|
public AjaxResult list(SysDept dept) |
||||
|
{ |
||||
|
List<SysDept> depts = deptService.selectDeptList(dept); |
||||
|
return success(depts); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询部门列表(排除节点) |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dept:list')") |
||||
|
@GetMapping("/list/exclude/{deptId}") |
||||
|
public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) |
||||
|
{ |
||||
|
List<SysDept> depts = deptService.selectDeptList(new SysDept()); |
||||
|
depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); |
||||
|
return success(depts); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据部门编号获取详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dept:query')") |
||||
|
@GetMapping(value = "/{deptId}") |
||||
|
public AjaxResult getInfo(@PathVariable Long deptId) |
||||
|
{ |
||||
|
deptService.checkDeptDataScope(deptId); |
||||
|
return success(deptService.selectDeptById(deptId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增部门 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dept:add')") |
||||
|
@Log(title = "部门管理", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysDept dept) |
||||
|
{ |
||||
|
if (!deptService.checkDeptNameUnique(dept)) |
||||
|
{ |
||||
|
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); |
||||
|
} |
||||
|
dept.setCreateBy(getUsername()); |
||||
|
return toAjax(deptService.insertDept(dept)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改部门 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dept:edit')") |
||||
|
@Log(title = "部门管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysDept dept) |
||||
|
{ |
||||
|
Long deptId = dept.getDeptId(); |
||||
|
deptService.checkDeptDataScope(deptId); |
||||
|
if (!deptService.checkDeptNameUnique(dept)) |
||||
|
{ |
||||
|
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); |
||||
|
} |
||||
|
else if (dept.getParentId().equals(deptId)) |
||||
|
{ |
||||
|
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); |
||||
|
} |
||||
|
else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) |
||||
|
{ |
||||
|
return error("该部门包含未停用的子部门!"); |
||||
|
} |
||||
|
dept.setUpdateBy(getUsername()); |
||||
|
return toAjax(deptService.updateDept(dept)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除部门 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dept:remove')") |
||||
|
@Log(title = "部门管理", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{deptId}") |
||||
|
public AjaxResult remove(@PathVariable Long deptId) |
||||
|
{ |
||||
|
if (deptService.hasChildByDeptId(deptId)) |
||||
|
{ |
||||
|
return warn("存在下级部门,不允许删除"); |
||||
|
} |
||||
|
if (deptService.checkDeptExistUser(deptId)) |
||||
|
{ |
||||
|
return warn("部门存在用户,不允许删除"); |
||||
|
} |
||||
|
deptService.checkDeptDataScope(deptId); |
||||
|
return toAjax(deptService.deleteDeptById(deptId)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,121 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysDictData; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.system.service.ISysDictDataService; |
||||
|
import com.chenhai.system.service.ISysDictTypeService; |
||||
|
|
||||
|
/** |
||||
|
* 数据字典信息 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/dict/data") |
||||
|
public class SysDictDataController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysDictDataService dictDataService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysDictTypeService dictTypeService; |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysDictData dictData) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysDictData> list = dictDataService.selectDictDataList(dictData); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "字典数据", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysDictData dictData) |
||||
|
{ |
||||
|
List<SysDictData> list = dictDataService.selectDictDataList(dictData); |
||||
|
ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class); |
||||
|
util.exportExcel(response, list, "字典数据"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询字典数据详细 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:query')") |
||||
|
@GetMapping(value = "/{dictCode}") |
||||
|
public AjaxResult getInfo(@PathVariable Long dictCode) |
||||
|
{ |
||||
|
return success(dictDataService.selectDictDataById(dictCode)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据字典类型查询字典数据信息 |
||||
|
*/ |
||||
|
@GetMapping(value = "/type/{dictType}") |
||||
|
public AjaxResult dictType(@PathVariable String dictType) |
||||
|
{ |
||||
|
List<SysDictData> data = dictTypeService.selectDictDataByType(dictType); |
||||
|
if (StringUtils.isNull(data)) |
||||
|
{ |
||||
|
data = new ArrayList<SysDictData>(); |
||||
|
} |
||||
|
return success(data); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增字典类型 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:add')") |
||||
|
@Log(title = "字典数据", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysDictData dict) |
||||
|
{ |
||||
|
dict.setCreateBy(getUsername()); |
||||
|
return toAjax(dictDataService.insertDictData(dict)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改保存字典类型 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:edit')") |
||||
|
@Log(title = "字典数据", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysDictData dict) |
||||
|
{ |
||||
|
dict.setUpdateBy(getUsername()); |
||||
|
return toAjax(dictDataService.updateDictData(dict)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除字典类型 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:remove')") |
||||
|
@Log(title = "字典类型", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{dictCodes}") |
||||
|
public AjaxResult remove(@PathVariable Long[] dictCodes) |
||||
|
{ |
||||
|
dictDataService.deleteDictDataByIds(dictCodes); |
||||
|
return success(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysDictType; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.system.service.ISysDictTypeService; |
||||
|
|
||||
|
/** |
||||
|
* 数据字典信息 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/dict/type") |
||||
|
public class SysDictTypeController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysDictTypeService dictTypeService; |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysDictType dictType) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "字典类型", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysDictType dictType) |
||||
|
{ |
||||
|
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType); |
||||
|
ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class); |
||||
|
util.exportExcel(response, list, "字典类型"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询字典类型详细 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:query')") |
||||
|
@GetMapping(value = "/{dictId}") |
||||
|
public AjaxResult getInfo(@PathVariable Long dictId) |
||||
|
{ |
||||
|
return success(dictTypeService.selectDictTypeById(dictId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增字典类型 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:add')") |
||||
|
@Log(title = "字典类型", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysDictType dict) |
||||
|
{ |
||||
|
if (!dictTypeService.checkDictTypeUnique(dict)) |
||||
|
{ |
||||
|
return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); |
||||
|
} |
||||
|
dict.setCreateBy(getUsername()); |
||||
|
return toAjax(dictTypeService.insertDictType(dict)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改字典类型 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:edit')") |
||||
|
@Log(title = "字典类型", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysDictType dict) |
||||
|
{ |
||||
|
if (!dictTypeService.checkDictTypeUnique(dict)) |
||||
|
{ |
||||
|
return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); |
||||
|
} |
||||
|
dict.setUpdateBy(getUsername()); |
||||
|
return toAjax(dictTypeService.updateDictType(dict)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除字典类型 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:remove')") |
||||
|
@Log(title = "字典类型", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{dictIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] dictIds) |
||||
|
{ |
||||
|
dictTypeService.deleteDictTypeByIds(dictIds); |
||||
|
return success(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 刷新字典缓存 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:dict:remove')") |
||||
|
@Log(title = "字典类型", businessType = BusinessType.CLEAN) |
||||
|
@DeleteMapping("/refreshCache") |
||||
|
public AjaxResult refreshCache() |
||||
|
{ |
||||
|
dictTypeService.resetDictCache(); |
||||
|
return success(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取字典选择框列表 |
||||
|
*/ |
||||
|
@GetMapping("/optionselect") |
||||
|
public AjaxResult optionselect() |
||||
|
{ |
||||
|
List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll(); |
||||
|
return success(dictTypes); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.config.RuoYiConfig; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
|
||||
|
/** |
||||
|
* 首页 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
public class SysIndexController |
||||
|
{ |
||||
|
/** 系统基础配置 */ |
||||
|
@Autowired |
||||
|
private RuoYiConfig ruoyiConfig; |
||||
|
|
||||
|
/** |
||||
|
* 访问首页,提示语 |
||||
|
*/ |
||||
|
@RequestMapping("/") |
||||
|
public String index() |
||||
|
{ |
||||
|
return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.constant.Constants; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysMenu; |
||||
|
import com.chenhai.common.core.domain.entity.SysUser; |
||||
|
import com.chenhai.common.core.domain.model.LoginBody; |
||||
|
import com.chenhai.common.core.domain.model.LoginUser; |
||||
|
import com.chenhai.common.core.text.Convert; |
||||
|
import com.chenhai.common.utils.DateUtils; |
||||
|
import com.chenhai.common.utils.SecurityUtils; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.framework.web.service.SysLoginService; |
||||
|
import com.chenhai.framework.web.service.SysPermissionService; |
||||
|
import com.chenhai.framework.web.service.TokenService; |
||||
|
import com.chenhai.system.service.ISysConfigService; |
||||
|
import com.chenhai.system.service.ISysMenuService; |
||||
|
|
||||
|
/** |
||||
|
* 登录验证 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
public class SysLoginController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private SysLoginService loginService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysMenuService menuService; |
||||
|
|
||||
|
@Autowired |
||||
|
private SysPermissionService permissionService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TokenService tokenService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysConfigService configService; |
||||
|
|
||||
|
/** |
||||
|
* 登录方法 |
||||
|
* |
||||
|
* @param loginBody 登录信息 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
@PostMapping("/login") |
||||
|
public AjaxResult login(@RequestBody LoginBody loginBody) |
||||
|
{ |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
// 生成令牌 |
||||
|
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), |
||||
|
loginBody.getUuid()); |
||||
|
ajax.put(Constants.TOKEN, token); |
||||
|
return ajax; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取用户信息 |
||||
|
* |
||||
|
* @return 用户信息 |
||||
|
*/ |
||||
|
@GetMapping("getInfo") |
||||
|
public AjaxResult getInfo() |
||||
|
{ |
||||
|
LoginUser loginUser = SecurityUtils.getLoginUser(); |
||||
|
SysUser user = loginUser.getUser(); |
||||
|
// 角色集合 |
||||
|
Set<String> roles = permissionService.getRolePermission(user); |
||||
|
// 权限集合 |
||||
|
Set<String> permissions = permissionService.getMenuPermission(user); |
||||
|
if (!loginUser.getPermissions().equals(permissions)) |
||||
|
{ |
||||
|
loginUser.setPermissions(permissions); |
||||
|
tokenService.refreshToken(loginUser); |
||||
|
} |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
ajax.put("user", user); |
||||
|
ajax.put("roles", roles); |
||||
|
ajax.put("permissions", permissions); |
||||
|
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate())); |
||||
|
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate())); |
||||
|
return ajax; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取路由信息 |
||||
|
* |
||||
|
* @return 路由信息 |
||||
|
*/ |
||||
|
@GetMapping("getRouters") |
||||
|
public AjaxResult getRouters() |
||||
|
{ |
||||
|
Long userId = SecurityUtils.getUserId(); |
||||
|
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId); |
||||
|
return AjaxResult.success(menuService.buildMenus(menus)); |
||||
|
} |
||||
|
|
||||
|
// 检查初始密码是否提醒修改 |
||||
|
public boolean initPasswordIsModify(Date pwdUpdateDate) |
||||
|
{ |
||||
|
Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify")); |
||||
|
return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null; |
||||
|
} |
||||
|
|
||||
|
// 检查密码是否过期 |
||||
|
public boolean passwordIsExpiration(Date pwdUpdateDate) |
||||
|
{ |
||||
|
Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays")); |
||||
|
if (passwordValidateDays != null && passwordValidateDays > 0) |
||||
|
{ |
||||
|
if (StringUtils.isNull(pwdUpdateDate)) |
||||
|
{ |
||||
|
// 如果从未修改过初始密码,直接提醒过期 |
||||
|
return true; |
||||
|
} |
||||
|
Date nowDate = DateUtils.getNowDate(); |
||||
|
return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,142 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.constant.UserConstants; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysMenu; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.system.service.ISysMenuService; |
||||
|
|
||||
|
/** |
||||
|
* 菜单信息 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/menu") |
||||
|
public class SysMenuController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysMenuService menuService; |
||||
|
|
||||
|
/** |
||||
|
* 获取菜单列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:menu:list')") |
||||
|
@GetMapping("/list") |
||||
|
public AjaxResult list(SysMenu menu) |
||||
|
{ |
||||
|
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId()); |
||||
|
return success(menus); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据菜单编号获取详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:menu:query')") |
||||
|
@GetMapping(value = "/{menuId}") |
||||
|
public AjaxResult getInfo(@PathVariable Long menuId) |
||||
|
{ |
||||
|
return success(menuService.selectMenuById(menuId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取菜单下拉树列表 |
||||
|
*/ |
||||
|
@GetMapping("/treeselect") |
||||
|
public AjaxResult treeselect(SysMenu menu) |
||||
|
{ |
||||
|
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId()); |
||||
|
return success(menuService.buildMenuTreeSelect(menus)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 加载对应角色菜单列表树 |
||||
|
*/ |
||||
|
@GetMapping(value = "/roleMenuTreeselect/{roleId}") |
||||
|
public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) |
||||
|
{ |
||||
|
List<SysMenu> menus = menuService.selectMenuList(getUserId()); |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); |
||||
|
ajax.put("menus", menuService.buildMenuTreeSelect(menus)); |
||||
|
return ajax; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增菜单 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:menu:add')") |
||||
|
@Log(title = "菜单管理", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysMenu menu) |
||||
|
{ |
||||
|
if (!menuService.checkMenuNameUnique(menu)) |
||||
|
{ |
||||
|
return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); |
||||
|
} |
||||
|
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) |
||||
|
{ |
||||
|
return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); |
||||
|
} |
||||
|
menu.setCreateBy(getUsername()); |
||||
|
return toAjax(menuService.insertMenu(menu)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改菜单 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:menu:edit')") |
||||
|
@Log(title = "菜单管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysMenu menu) |
||||
|
{ |
||||
|
if (!menuService.checkMenuNameUnique(menu)) |
||||
|
{ |
||||
|
return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); |
||||
|
} |
||||
|
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) |
||||
|
{ |
||||
|
return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); |
||||
|
} |
||||
|
else if (menu.getMenuId().equals(menu.getParentId())) |
||||
|
{ |
||||
|
return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); |
||||
|
} |
||||
|
menu.setUpdateBy(getUsername()); |
||||
|
return toAjax(menuService.updateMenu(menu)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除菜单 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:menu:remove')") |
||||
|
@Log(title = "菜单管理", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{menuId}") |
||||
|
public AjaxResult remove(@PathVariable("menuId") Long menuId) |
||||
|
{ |
||||
|
if (menuService.hasChildByMenuId(menuId)) |
||||
|
{ |
||||
|
return warn("存在子菜单,不允许删除"); |
||||
|
} |
||||
|
if (menuService.checkMenuExistRole(menuId)) |
||||
|
{ |
||||
|
return warn("菜单已分配,不允许删除"); |
||||
|
} |
||||
|
return toAjax(menuService.deleteMenuById(menuId)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.system.domain.SysNotice; |
||||
|
import com.chenhai.system.service.ISysNoticeService; |
||||
|
|
||||
|
/** |
||||
|
* 公告 信息操作处理 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/notice") |
||||
|
public class SysNoticeController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysNoticeService noticeService; |
||||
|
|
||||
|
/** |
||||
|
* 获取通知公告列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:notice:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysNotice notice) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysNotice> list = noticeService.selectNoticeList(notice); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据通知公告编号获取详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:notice:query')") |
||||
|
@GetMapping(value = "/{noticeId}") |
||||
|
public AjaxResult getInfo(@PathVariable Long noticeId) |
||||
|
{ |
||||
|
return success(noticeService.selectNoticeById(noticeId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增通知公告 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:notice:add')") |
||||
|
@Log(title = "通知公告", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysNotice notice) |
||||
|
{ |
||||
|
notice.setCreateBy(getUsername()); |
||||
|
return toAjax(noticeService.insertNotice(notice)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改通知公告 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:notice:edit')") |
||||
|
@Log(title = "通知公告", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysNotice notice) |
||||
|
{ |
||||
|
notice.setUpdateBy(getUsername()); |
||||
|
return toAjax(noticeService.updateNotice(notice)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除通知公告 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:notice:remove')") |
||||
|
@Log(title = "通知公告", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{noticeIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] noticeIds) |
||||
|
{ |
||||
|
return toAjax(noticeService.deleteNoticeByIds(noticeIds)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.system.domain.SysPost; |
||||
|
import com.chenhai.system.service.ISysPostService; |
||||
|
|
||||
|
/** |
||||
|
* 岗位信息操作处理 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/post") |
||||
|
public class SysPostController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysPostService postService; |
||||
|
|
||||
|
/** |
||||
|
* 获取岗位列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:post:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysPost post) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysPost> list = postService.selectPostList(post); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "岗位管理", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('system:post:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysPost post) |
||||
|
{ |
||||
|
List<SysPost> list = postService.selectPostList(post); |
||||
|
ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class); |
||||
|
util.exportExcel(response, list, "岗位数据"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据岗位编号获取详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:post:query')") |
||||
|
@GetMapping(value = "/{postId}") |
||||
|
public AjaxResult getInfo(@PathVariable Long postId) |
||||
|
{ |
||||
|
return success(postService.selectPostById(postId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增岗位 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:post:add')") |
||||
|
@Log(title = "岗位管理", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysPost post) |
||||
|
{ |
||||
|
if (!postService.checkPostNameUnique(post)) |
||||
|
{ |
||||
|
return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); |
||||
|
} |
||||
|
else if (!postService.checkPostCodeUnique(post)) |
||||
|
{ |
||||
|
return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); |
||||
|
} |
||||
|
post.setCreateBy(getUsername()); |
||||
|
return toAjax(postService.insertPost(post)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改岗位 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:post:edit')") |
||||
|
@Log(title = "岗位管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysPost post) |
||||
|
{ |
||||
|
if (!postService.checkPostNameUnique(post)) |
||||
|
{ |
||||
|
return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); |
||||
|
} |
||||
|
else if (!postService.checkPostCodeUnique(post)) |
||||
|
{ |
||||
|
return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); |
||||
|
} |
||||
|
post.setUpdateBy(getUsername()); |
||||
|
return toAjax(postService.updatePost(post)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除岗位 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:post:remove')") |
||||
|
@Log(title = "岗位管理", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{postIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] postIds) |
||||
|
{ |
||||
|
return toAjax(postService.deletePostByIds(postIds)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取岗位选择框列表 |
||||
|
*/ |
||||
|
@GetMapping("/optionselect") |
||||
|
public AjaxResult optionselect() |
||||
|
{ |
||||
|
List<SysPost> posts = postService.selectPostAll(); |
||||
|
return success(posts); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,148 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestParam; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.config.RuoYiConfig; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysUser; |
||||
|
import com.chenhai.common.core.domain.model.LoginUser; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.DateUtils; |
||||
|
import com.chenhai.common.utils.SecurityUtils; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.common.utils.file.FileUploadUtils; |
||||
|
import com.chenhai.common.utils.file.FileUtils; |
||||
|
import com.chenhai.common.utils.file.MimeTypeUtils; |
||||
|
import com.chenhai.framework.web.service.TokenService; |
||||
|
import com.chenhai.system.service.ISysUserService; |
||||
|
|
||||
|
/** |
||||
|
* 个人信息 业务处理 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/user/profile") |
||||
|
public class SysProfileController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysUserService userService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TokenService tokenService; |
||||
|
|
||||
|
/** |
||||
|
* 个人信息 |
||||
|
*/ |
||||
|
@GetMapping |
||||
|
public AjaxResult profile() |
||||
|
{ |
||||
|
LoginUser loginUser = getLoginUser(); |
||||
|
SysUser user = loginUser.getUser(); |
||||
|
AjaxResult ajax = AjaxResult.success(user); |
||||
|
ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); |
||||
|
ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); |
||||
|
return ajax; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改用户 |
||||
|
*/ |
||||
|
@Log(title = "个人信息", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult updateProfile(@RequestBody SysUser user) |
||||
|
{ |
||||
|
LoginUser loginUser = getLoginUser(); |
||||
|
SysUser currentUser = loginUser.getUser(); |
||||
|
currentUser.setNickName(user.getNickName()); |
||||
|
currentUser.setEmail(user.getEmail()); |
||||
|
currentUser.setPhonenumber(user.getPhonenumber()); |
||||
|
currentUser.setSex(user.getSex()); |
||||
|
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) |
||||
|
{ |
||||
|
return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在"); |
||||
|
} |
||||
|
if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) |
||||
|
{ |
||||
|
return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在"); |
||||
|
} |
||||
|
if (userService.updateUserProfile(currentUser) > 0) |
||||
|
{ |
||||
|
// 更新缓存用户信息 |
||||
|
tokenService.setLoginUser(loginUser); |
||||
|
return success(); |
||||
|
} |
||||
|
return error("修改个人信息异常,请联系管理员"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重置密码 |
||||
|
*/ |
||||
|
@Log(title = "个人信息", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping("/updatePwd") |
||||
|
public AjaxResult updatePwd(@RequestBody Map<String, String> params) |
||||
|
{ |
||||
|
String oldPassword = params.get("oldPassword"); |
||||
|
String newPassword = params.get("newPassword"); |
||||
|
LoginUser loginUser = getLoginUser(); |
||||
|
Long userId = loginUser.getUserId(); |
||||
|
String password = loginUser.getPassword(); |
||||
|
if (!SecurityUtils.matchesPassword(oldPassword, password)) |
||||
|
{ |
||||
|
return error("修改密码失败,旧密码错误"); |
||||
|
} |
||||
|
if (SecurityUtils.matchesPassword(newPassword, password)) |
||||
|
{ |
||||
|
return error("新密码不能与旧密码相同"); |
||||
|
} |
||||
|
newPassword = SecurityUtils.encryptPassword(newPassword); |
||||
|
if (userService.resetUserPwd(userId, newPassword) > 0) |
||||
|
{ |
||||
|
// 更新缓存用户密码&密码最后更新时间 |
||||
|
loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate()); |
||||
|
loginUser.getUser().setPassword(newPassword); |
||||
|
tokenService.setLoginUser(loginUser); |
||||
|
return success(); |
||||
|
} |
||||
|
return error("修改密码异常,请联系管理员"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 头像上传 |
||||
|
*/ |
||||
|
@Log(title = "用户头像", businessType = BusinessType.UPDATE) |
||||
|
@PostMapping("/avatar") |
||||
|
public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception |
||||
|
{ |
||||
|
if (!file.isEmpty()) |
||||
|
{ |
||||
|
LoginUser loginUser = getLoginUser(); |
||||
|
String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true); |
||||
|
if (userService.updateUserAvatar(loginUser.getUserId(), avatar)) |
||||
|
{ |
||||
|
String oldAvatar = loginUser.getUser().getAvatar(); |
||||
|
if (StringUtils.isNotEmpty(oldAvatar)) |
||||
|
{ |
||||
|
FileUtils.deleteFile(RuoYiConfig.getProfile() + FileUtils.stripPrefix(oldAvatar)); |
||||
|
} |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
ajax.put("imgUrl", avatar); |
||||
|
// 更新缓存用户头像 |
||||
|
loginUser.getUser().setAvatar(avatar); |
||||
|
tokenService.setLoginUser(loginUser); |
||||
|
return ajax; |
||||
|
} |
||||
|
} |
||||
|
return error("上传图片异常,请联系管理员"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.model.RegisterBody; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.framework.web.service.SysRegisterService; |
||||
|
import com.chenhai.system.service.ISysConfigService; |
||||
|
|
||||
|
/** |
||||
|
* 注册验证 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
public class SysRegisterController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private SysRegisterService registerService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysConfigService configService; |
||||
|
|
||||
|
@PostMapping("/register") |
||||
|
public AjaxResult register(@RequestBody RegisterBody user) |
||||
|
{ |
||||
|
if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) |
||||
|
{ |
||||
|
return error("当前系统没有开启注册功能!"); |
||||
|
} |
||||
|
String msg = registerService.register(user); |
||||
|
return StringUtils.isEmpty(msg) ? success() : error(msg); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,262 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysDept; |
||||
|
import com.chenhai.common.core.domain.entity.SysRole; |
||||
|
import com.chenhai.common.core.domain.entity.SysUser; |
||||
|
import com.chenhai.common.core.domain.model.LoginUser; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.framework.web.service.SysPermissionService; |
||||
|
import com.chenhai.framework.web.service.TokenService; |
||||
|
import com.chenhai.system.domain.SysUserRole; |
||||
|
import com.chenhai.system.service.ISysDeptService; |
||||
|
import com.chenhai.system.service.ISysRoleService; |
||||
|
import com.chenhai.system.service.ISysUserService; |
||||
|
|
||||
|
/** |
||||
|
* 角色信息 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/role") |
||||
|
public class SysRoleController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysRoleService roleService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TokenService tokenService; |
||||
|
|
||||
|
@Autowired |
||||
|
private SysPermissionService permissionService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysUserService userService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysDeptService deptService; |
||||
|
|
||||
|
@PreAuthorize("@ss.hasPermi('system:role:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysRole role) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysRole> list = roleService.selectRoleList(role); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "角色管理", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysRole role) |
||||
|
{ |
||||
|
List<SysRole> list = roleService.selectRoleList(role); |
||||
|
ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class); |
||||
|
util.exportExcel(response, list, "角色数据"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据角色编号获取详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:query')") |
||||
|
@GetMapping(value = "/{roleId}") |
||||
|
public AjaxResult getInfo(@PathVariable Long roleId) |
||||
|
{ |
||||
|
roleService.checkRoleDataScope(roleId); |
||||
|
return success(roleService.selectRoleById(roleId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增角色 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:add')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysRole role) |
||||
|
{ |
||||
|
if (!roleService.checkRoleNameUnique(role)) |
||||
|
{ |
||||
|
return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); |
||||
|
} |
||||
|
else if (!roleService.checkRoleKeyUnique(role)) |
||||
|
{ |
||||
|
return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); |
||||
|
} |
||||
|
role.setCreateBy(getUsername()); |
||||
|
return toAjax(roleService.insertRole(role)); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改保存角色 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:edit')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysRole role) |
||||
|
{ |
||||
|
roleService.checkRoleAllowed(role); |
||||
|
roleService.checkRoleDataScope(role.getRoleId()); |
||||
|
if (!roleService.checkRoleNameUnique(role)) |
||||
|
{ |
||||
|
return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); |
||||
|
} |
||||
|
else if (!roleService.checkRoleKeyUnique(role)) |
||||
|
{ |
||||
|
return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); |
||||
|
} |
||||
|
role.setUpdateBy(getUsername()); |
||||
|
|
||||
|
if (roleService.updateRole(role) > 0) |
||||
|
{ |
||||
|
// 更新缓存用户权限 |
||||
|
LoginUser loginUser = getLoginUser(); |
||||
|
if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) |
||||
|
{ |
||||
|
loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName())); |
||||
|
loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); |
||||
|
tokenService.setLoginUser(loginUser); |
||||
|
} |
||||
|
return success(); |
||||
|
} |
||||
|
return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改保存数据权限 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:edit')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping("/dataScope") |
||||
|
public AjaxResult dataScope(@RequestBody SysRole role) |
||||
|
{ |
||||
|
roleService.checkRoleAllowed(role); |
||||
|
roleService.checkRoleDataScope(role.getRoleId()); |
||||
|
return toAjax(roleService.authDataScope(role)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 状态修改 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:edit')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping("/changeStatus") |
||||
|
public AjaxResult changeStatus(@RequestBody SysRole role) |
||||
|
{ |
||||
|
roleService.checkRoleAllowed(role); |
||||
|
roleService.checkRoleDataScope(role.getRoleId()); |
||||
|
role.setUpdateBy(getUsername()); |
||||
|
return toAjax(roleService.updateRoleStatus(role)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除角色 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:remove')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{roleIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] roleIds) |
||||
|
{ |
||||
|
return toAjax(roleService.deleteRoleByIds(roleIds)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取角色选择框列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:query')") |
||||
|
@GetMapping("/optionselect") |
||||
|
public AjaxResult optionselect() |
||||
|
{ |
||||
|
return success(roleService.selectRoleAll()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询已分配用户角色列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:list')") |
||||
|
@GetMapping("/authUser/allocatedList") |
||||
|
public TableDataInfo allocatedList(SysUser user) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysUser> list = userService.selectAllocatedList(user); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询未分配用户角色列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:list')") |
||||
|
@GetMapping("/authUser/unallocatedList") |
||||
|
public TableDataInfo unallocatedList(SysUser user) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysUser> list = userService.selectUnallocatedList(user); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 取消授权用户 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:edit')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.GRANT) |
||||
|
@PutMapping("/authUser/cancel") |
||||
|
public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) |
||||
|
{ |
||||
|
return toAjax(roleService.deleteAuthUser(userRole)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量取消授权用户 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:edit')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.GRANT) |
||||
|
@PutMapping("/authUser/cancelAll") |
||||
|
public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) |
||||
|
{ |
||||
|
return toAjax(roleService.deleteAuthUsers(roleId, userIds)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量选择用户授权 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:edit')") |
||||
|
@Log(title = "角色管理", businessType = BusinessType.GRANT) |
||||
|
@PutMapping("/authUser/selectAll") |
||||
|
public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) |
||||
|
{ |
||||
|
roleService.checkRoleDataScope(roleId); |
||||
|
return toAjax(roleService.insertAuthUsers(roleId, userIds)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取对应角色部门树列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:role:query')") |
||||
|
@GetMapping(value = "/deptTree/{roleId}") |
||||
|
public AjaxResult deptTree(@PathVariable("roleId") Long roleId) |
||||
|
{ |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); |
||||
|
ajax.put("depts", deptService.selectDeptTreeList(new SysDept())); |
||||
|
return ajax; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,256 @@ |
|||||
|
package com.chenhai.web.controller.system; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.stream.Collectors; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.apache.commons.lang3.ArrayUtils; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.validation.annotation.Validated; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.domain.entity.SysDept; |
||||
|
import com.chenhai.common.core.domain.entity.SysRole; |
||||
|
import com.chenhai.common.core.domain.entity.SysUser; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.common.utils.SecurityUtils; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.system.service.ISysDeptService; |
||||
|
import com.chenhai.system.service.ISysPostService; |
||||
|
import com.chenhai.system.service.ISysRoleService; |
||||
|
import com.chenhai.system.service.ISysUserService; |
||||
|
|
||||
|
/** |
||||
|
* 用户信息 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/system/user") |
||||
|
public class SysUserController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private ISysUserService userService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysRoleService roleService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysDeptService deptService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ISysPostService postService; |
||||
|
|
||||
|
/** |
||||
|
* 获取用户列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(SysUser user) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<SysUser> list = userService.selectUserList(user); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "用户管理", businessType = BusinessType.EXPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:export')") |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, SysUser user) |
||||
|
{ |
||||
|
List<SysUser> list = userService.selectUserList(user); |
||||
|
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); |
||||
|
util.exportExcel(response, list, "用户数据"); |
||||
|
} |
||||
|
|
||||
|
@Log(title = "用户管理", businessType = BusinessType.IMPORT) |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:import')") |
||||
|
@PostMapping("/importData") |
||||
|
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception |
||||
|
{ |
||||
|
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); |
||||
|
List<SysUser> userList = util.importExcel(file.getInputStream()); |
||||
|
String operName = getUsername(); |
||||
|
String message = userService.importUser(userList, updateSupport, operName); |
||||
|
return success(message); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/importTemplate") |
||||
|
public void importTemplate(HttpServletResponse response) |
||||
|
{ |
||||
|
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); |
||||
|
util.importTemplateExcel(response, "用户数据"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据用户编号获取详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:query')") |
||||
|
@GetMapping(value = { "/", "/{userId}" }) |
||||
|
public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) |
||||
|
{ |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
if (StringUtils.isNotNull(userId)) |
||||
|
{ |
||||
|
userService.checkUserDataScope(userId); |
||||
|
SysUser sysUser = userService.selectUserById(userId); |
||||
|
ajax.put(AjaxResult.DATA_TAG, sysUser); |
||||
|
ajax.put("postIds", postService.selectPostListByUserId(userId)); |
||||
|
ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); |
||||
|
} |
||||
|
List<SysRole> roles = roleService.selectRoleAll(); |
||||
|
ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); |
||||
|
ajax.put("posts", postService.selectPostAll()); |
||||
|
return ajax; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增用户 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:add')") |
||||
|
@Log(title = "用户管理", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@Validated @RequestBody SysUser user) |
||||
|
{ |
||||
|
deptService.checkDeptDataScope(user.getDeptId()); |
||||
|
roleService.checkRoleDataScope(user.getRoleIds()); |
||||
|
if (!userService.checkUserNameUnique(user)) |
||||
|
{ |
||||
|
return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); |
||||
|
} |
||||
|
else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) |
||||
|
{ |
||||
|
return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); |
||||
|
} |
||||
|
else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) |
||||
|
{ |
||||
|
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); |
||||
|
} |
||||
|
user.setCreateBy(getUsername()); |
||||
|
user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); |
||||
|
return toAjax(userService.insertUser(user)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改用户 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:edit')") |
||||
|
@Log(title = "用户管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@Validated @RequestBody SysUser user) |
||||
|
{ |
||||
|
userService.checkUserAllowed(user); |
||||
|
userService.checkUserDataScope(user.getUserId()); |
||||
|
deptService.checkDeptDataScope(user.getDeptId()); |
||||
|
roleService.checkRoleDataScope(user.getRoleIds()); |
||||
|
if (!userService.checkUserNameUnique(user)) |
||||
|
{ |
||||
|
return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); |
||||
|
} |
||||
|
else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) |
||||
|
{ |
||||
|
return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); |
||||
|
} |
||||
|
else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) |
||||
|
{ |
||||
|
return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); |
||||
|
} |
||||
|
user.setUpdateBy(getUsername()); |
||||
|
return toAjax(userService.updateUser(user)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除用户 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:remove')") |
||||
|
@Log(title = "用户管理", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{userIds}") |
||||
|
public AjaxResult remove(@PathVariable Long[] userIds) |
||||
|
{ |
||||
|
if (ArrayUtils.contains(userIds, getUserId())) |
||||
|
{ |
||||
|
return error("当前用户不能删除"); |
||||
|
} |
||||
|
return toAjax(userService.deleteUserByIds(userIds)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重置密码 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:resetPwd')") |
||||
|
@Log(title = "用户管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping("/resetPwd") |
||||
|
public AjaxResult resetPwd(@RequestBody SysUser user) |
||||
|
{ |
||||
|
userService.checkUserAllowed(user); |
||||
|
userService.checkUserDataScope(user.getUserId()); |
||||
|
user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); |
||||
|
user.setUpdateBy(getUsername()); |
||||
|
return toAjax(userService.resetPwd(user)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 状态修改 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:edit')") |
||||
|
@Log(title = "用户管理", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping("/changeStatus") |
||||
|
public AjaxResult changeStatus(@RequestBody SysUser user) |
||||
|
{ |
||||
|
userService.checkUserAllowed(user); |
||||
|
userService.checkUserDataScope(user.getUserId()); |
||||
|
user.setUpdateBy(getUsername()); |
||||
|
return toAjax(userService.updateUserStatus(user)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据用户编号获取授权角色 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:query')") |
||||
|
@GetMapping("/authRole/{userId}") |
||||
|
public AjaxResult authRole(@PathVariable("userId") Long userId) |
||||
|
{ |
||||
|
AjaxResult ajax = AjaxResult.success(); |
||||
|
SysUser user = userService.selectUserById(userId); |
||||
|
List<SysRole> roles = roleService.selectRolesByUserId(userId); |
||||
|
ajax.put("user", user); |
||||
|
ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); |
||||
|
return ajax; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 用户授权角色 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:edit')") |
||||
|
@Log(title = "用户管理", businessType = BusinessType.GRANT) |
||||
|
@PutMapping("/authRole") |
||||
|
public AjaxResult insertAuthRole(Long userId, Long[] roleIds) |
||||
|
{ |
||||
|
userService.checkUserDataScope(userId); |
||||
|
roleService.checkRoleDataScope(roleIds); |
||||
|
userService.insertUserAuth(userId, roleIds); |
||||
|
return success(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取部门树列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('system:user:list')") |
||||
|
@GetMapping("/deptTree") |
||||
|
public AjaxResult deptTree(SysDept dept) |
||||
|
{ |
||||
|
return success(deptService.selectDeptTreeList(dept)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,175 @@ |
|||||
|
package com.chenhai.web.controller.tool; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.LinkedHashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.R; |
||||
|
import com.chenhai.common.utils.StringUtils; |
||||
|
import io.swagger.v3.oas.annotations.Operation; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import io.swagger.v3.oas.annotations.tags.Tag; |
||||
|
|
||||
|
/** |
||||
|
* swagger 用户测试方法 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@Tag(name = "用户信息管理") |
||||
|
@RestController |
||||
|
@RequestMapping("/test/user") |
||||
|
public class TestController extends BaseController |
||||
|
{ |
||||
|
private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>(); |
||||
|
{ |
||||
|
users.put(1, new UserEntity(1, "admin", "admin123", "15888888888")); |
||||
|
users.put(2, new UserEntity(2, "ry", "admin123", "15666666666")); |
||||
|
} |
||||
|
|
||||
|
@Operation(summary = "获取用户列表") |
||||
|
@GetMapping("/list") |
||||
|
public R<List<UserEntity>> userList() |
||||
|
{ |
||||
|
List<UserEntity> userList = new ArrayList<UserEntity>(users.values()); |
||||
|
return R.ok(userList); |
||||
|
} |
||||
|
|
||||
|
@Operation(summary = "获取用户详细") |
||||
|
@GetMapping("/{userId}") |
||||
|
public R<UserEntity> getUser(@PathVariable(name = "userId") |
||||
|
Integer userId) |
||||
|
{ |
||||
|
if (!users.isEmpty() && users.containsKey(userId)) |
||||
|
{ |
||||
|
return R.ok(users.get(userId)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return R.fail("用户不存在"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Operation(summary = "新增用户") |
||||
|
@PostMapping("/save") |
||||
|
public R<String> save(UserEntity user) |
||||
|
{ |
||||
|
if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) |
||||
|
{ |
||||
|
return R.fail("用户ID不能为空"); |
||||
|
} |
||||
|
users.put(user.getUserId(), user); |
||||
|
return R.ok(); |
||||
|
} |
||||
|
|
||||
|
@Operation(summary = "更新用户") |
||||
|
@PutMapping("/update") |
||||
|
public R<String> update(@RequestBody |
||||
|
UserEntity user) |
||||
|
{ |
||||
|
if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) |
||||
|
{ |
||||
|
return R.fail("用户ID不能为空"); |
||||
|
} |
||||
|
if (users.isEmpty() || !users.containsKey(user.getUserId())) |
||||
|
{ |
||||
|
return R.fail("用户不存在"); |
||||
|
} |
||||
|
users.remove(user.getUserId()); |
||||
|
users.put(user.getUserId(), user); |
||||
|
return R.ok(); |
||||
|
} |
||||
|
|
||||
|
@Operation(summary = "删除用户信息") |
||||
|
@DeleteMapping("/{userId}") |
||||
|
public R<String> delete(@PathVariable(name = "userId") |
||||
|
Integer userId) |
||||
|
{ |
||||
|
if (!users.isEmpty() && users.containsKey(userId)) |
||||
|
{ |
||||
|
users.remove(userId); |
||||
|
return R.ok(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return R.fail("用户不存在"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Schema(description = "用户实体") |
||||
|
class UserEntity |
||||
|
{ |
||||
|
@Schema(title = "用户ID") |
||||
|
private Integer userId; |
||||
|
|
||||
|
@Schema(title = "用户名称") |
||||
|
private String username; |
||||
|
|
||||
|
@Schema(title = "用户密码") |
||||
|
private String password; |
||||
|
|
||||
|
@Schema(title = "用户手机") |
||||
|
private String mobile; |
||||
|
|
||||
|
public UserEntity() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public UserEntity(Integer userId, String username, String password, String mobile) |
||||
|
{ |
||||
|
this.userId = userId; |
||||
|
this.username = username; |
||||
|
this.password = password; |
||||
|
this.mobile = mobile; |
||||
|
} |
||||
|
|
||||
|
public Integer getUserId() |
||||
|
{ |
||||
|
return userId; |
||||
|
} |
||||
|
|
||||
|
public void setUserId(Integer userId) |
||||
|
{ |
||||
|
this.userId = userId; |
||||
|
} |
||||
|
|
||||
|
public String getUsername() |
||||
|
{ |
||||
|
return username; |
||||
|
} |
||||
|
|
||||
|
public void setUsername(String username) |
||||
|
{ |
||||
|
this.username = username; |
||||
|
} |
||||
|
|
||||
|
public String getPassword() |
||||
|
{ |
||||
|
return password; |
||||
|
} |
||||
|
|
||||
|
public void setPassword(String password) |
||||
|
{ |
||||
|
this.password = password; |
||||
|
} |
||||
|
|
||||
|
public String getMobile() |
||||
|
{ |
||||
|
return mobile; |
||||
|
} |
||||
|
|
||||
|
public void setMobile(String mobile) |
||||
|
{ |
||||
|
this.mobile = mobile; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
package com.chenhai.web.core.config; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import com.chenhai.common.config.RuoYiConfig; |
||||
|
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.security.SecurityRequirement; |
||||
|
import io.swagger.v3.oas.models.security.SecurityScheme; |
||||
|
|
||||
|
/** |
||||
|
* Swagger2的接口配置 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
*/ |
||||
|
@Configuration |
||||
|
public class SwaggerConfig |
||||
|
{ |
||||
|
/** 系统基础配置 */ |
||||
|
@Autowired |
||||
|
private RuoYiConfig ruoyiConfig; |
||||
|
|
||||
|
/** |
||||
|
* 自定义的 OpenAPI 对象 |
||||
|
*/ |
||||
|
@Bean |
||||
|
public OpenAPI customOpenApi() |
||||
|
{ |
||||
|
return new OpenAPI().components(new Components() |
||||
|
// 设置认证的请求头 |
||||
|
.addSecuritySchemes("apikey", securityScheme())) |
||||
|
.addSecurityItem(new SecurityRequirement().addList("apikey")) |
||||
|
.info(getApiInfo()); |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
public SecurityScheme securityScheme() |
||||
|
{ |
||||
|
return new SecurityScheme() |
||||
|
.type(SecurityScheme.Type.APIKEY) |
||||
|
.name("Authorization") |
||||
|
.in(SecurityScheme.In.HEADER) |
||||
|
.scheme("Bearer"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加摘要信息 |
||||
|
*/ |
||||
|
public Info getApiInfo() |
||||
|
{ |
||||
|
return new Info() |
||||
|
// 设置标题 |
||||
|
.title("标题:若依管理系统_接口文档") |
||||
|
// 描述 |
||||
|
.description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...") |
||||
|
// 作者信息 |
||||
|
.contact(new Contact().name(ruoyiConfig.getName())) |
||||
|
// 版本 |
||||
|
.version("版本号:" + ruoyiConfig.getVersion()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
restart.include.json=/com.alibaba.fastjson2.*.jar |
||||
@ -0,0 +1,108 @@ |
|||||
|
# 数据源配置 |
||||
|
spring: |
||||
|
datasource: |
||||
|
type: com.alibaba.druid.pool.DruidDataSource |
||||
|
driverClassName: com.mysql.cj.jdbc.Driver |
||||
|
druid: |
||||
|
# 主库数据源 |
||||
|
master: |
||||
|
url: jdbc:mysql://localhost:3306/chenhai_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 |
||||
|
username: root |
||||
|
password: root |
||||
|
# url: jdbc:mysql://localhost:3307/ruoyi?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 |
||||
|
# username: root |
||||
|
# password: root |
||||
|
# 从库数据源 |
||||
|
slave: |
||||
|
# 从数据源开关/默认关闭 |
||||
|
enabled: false |
||||
|
url: jdbc:mysql://localhost:3306/erp?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 |
||||
|
username: root |
||||
|
password: root |
||||
|
# 初始连接数 |
||||
|
initialSize: 5 |
||||
|
# 最小连接池数量 |
||||
|
minIdle: 10 |
||||
|
# 最大连接池数量 |
||||
|
maxActive: 20 |
||||
|
# 配置获取连接等待超时的时间 |
||||
|
maxWait: 60000 |
||||
|
# 配置连接超时时间 |
||||
|
connectTimeout: 30000 |
||||
|
# 配置网络超时时间 |
||||
|
socketTimeout: 60000 |
||||
|
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 |
||||
|
timeBetweenEvictionRunsMillis: 60000 |
||||
|
# 配置一个连接在池中最小生存的时间,单位是毫秒 |
||||
|
minEvictableIdleTimeMillis: 300000 |
||||
|
# 配置一个连接在池中最大生存的时间,单位是毫秒 |
||||
|
maxEvictableIdleTimeMillis: 900000 |
||||
|
# 配置检测连接是否有效 |
||||
|
validationQuery: SELECT 1 FROM DUAL |
||||
|
testWhileIdle: true |
||||
|
testOnBorrow: false |
||||
|
testOnReturn: false |
||||
|
webStatFilter: |
||||
|
enabled: true |
||||
|
statViewServlet: |
||||
|
enabled: true |
||||
|
# 设置白名单,不填则允许所有访问 |
||||
|
allow: |
||||
|
url-pattern: /druid/* |
||||
|
# 控制台管理用户名和密码 |
||||
|
login-username: ruoyi |
||||
|
login-password: 123456 |
||||
|
filter: |
||||
|
stat: |
||||
|
enabled: true |
||||
|
# 慢SQL记录 |
||||
|
log-slow-sql: true |
||||
|
slow-sql-millis: 1000 |
||||
|
merge-sql: true |
||||
|
wall: |
||||
|
config: |
||||
|
multi-statement-allow: true |
||||
|
|
||||
|
|
||||
|
|
||||
|
ai: |
||||
|
zhipuai: |
||||
|
api-key: e24ed227aff14409b2cc5b0ee7f97df8.7vryPYluxxmvtl7z |
||||
|
base-url: "https://open.bigmodel.cn/api/paas" |
||||
|
chat: |
||||
|
options: |
||||
|
model: glm-4-flash |
||||
|
# embedding: |
||||
|
# enabled: false |
||||
|
|
||||
|
embedding: |
||||
|
options: |
||||
|
model: embedding-3 # 使用的嵌入模型名称(embedding-3) |
||||
|
dimensions: 256 # 嵌入向量的维度(256维) |
||||
|
|
||||
|
# ai: |
||||
|
# ollama: |
||||
|
# base-url: http://127.0.0.1:11434 |
||||
|
# chat: |
||||
|
# options: |
||||
|
# model: qwen3:4b |
||||
|
|
||||
|
ollama: |
||||
|
base-url: http://172.16.1.165:11434 |
||||
|
chat: |
||||
|
model: deepseek-r1:14b |
||||
|
# mcp: |
||||
|
# client: |
||||
|
# request-timeout: 30s |
||||
|
# toolcallback: |
||||
|
# enabled: true |
||||
|
# stdio: |
||||
|
# servers-configuration: classpath:/mcp-servers.json |
||||
|
|
||||
|
|
||||
|
#logging: |
||||
|
# level: |
||||
|
# org.springframework.web.reactive.function.client: TRACE |
||||
|
# org.springframework.ai: DEBUG |
||||
|
# org.springframework.ai.client: DEBUG |
||||
|
|
||||
@ -0,0 +1,148 @@ |
|||||
|
# 项目相关配置 |
||||
|
ruoyi: |
||||
|
# 名称 |
||||
|
name: RuoYi |
||||
|
# 版本 |
||||
|
version: 3.9.0 |
||||
|
# 版权年份 |
||||
|
copyrightYear: 2025 |
||||
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) |
||||
|
profile: D:/ruoyi/uploadPath |
||||
|
# 获取ip地址开关 |
||||
|
addressEnabled: false |
||||
|
# 验证码类型 math 数字计算 char 字符验证 |
||||
|
captchaType: math |
||||
|
|
||||
|
# 开发环境配置 |
||||
|
server: |
||||
|
# 服务器的HTTP端口,默认为8080 |
||||
|
port: 8081 |
||||
|
servlet: |
||||
|
encoding: |
||||
|
charset: UTF-8 |
||||
|
enabled: true |
||||
|
force: true |
||||
|
# 应用的访问路径 |
||||
|
context-path: / |
||||
|
tomcat: |
||||
|
# tomcat的URI编码 |
||||
|
uri-encoding: UTF-8 |
||||
|
# 连接数满后的排队数,默认为100 |
||||
|
accept-count: 1000 |
||||
|
threads: |
||||
|
# tomcat最大线程数,默认为200 |
||||
|
max: 800 |
||||
|
# Tomcat启动初始化的线程数,默认值10 |
||||
|
min-spare: 100 |
||||
|
|
||||
|
# 日志配置 |
||||
|
logging: |
||||
|
level: |
||||
|
com.chenhai: debug |
||||
|
org.springframework: warn |
||||
|
|
||||
|
# 用户配置 |
||||
|
user: |
||||
|
password: |
||||
|
# 密码最大错误次数 |
||||
|
maxRetryCount: 5 |
||||
|
# 密码锁定时间(默认10分钟) |
||||
|
lockTime: 10 |
||||
|
|
||||
|
# Spring配置 |
||||
|
spring: |
||||
|
# 资源信息 |
||||
|
messages: |
||||
|
# 国际化资源文件路径 |
||||
|
basename: i18n/messages |
||||
|
profiles: |
||||
|
active: druid |
||||
|
# 文件上传 |
||||
|
servlet: |
||||
|
multipart: |
||||
|
# 单个文件大小 |
||||
|
max-file-size: 10MB |
||||
|
# 设置总上传的文件大小 |
||||
|
max-request-size: 20MB |
||||
|
# 服务模块 |
||||
|
devtools: |
||||
|
restart: |
||||
|
# 热部署开关 |
||||
|
enabled: true |
||||
|
data: |
||||
|
# redis 配置 |
||||
|
redis: |
||||
|
# 地址 |
||||
|
host: localhost |
||||
|
# 端口,默认为6379 |
||||
|
port: 6379 |
||||
|
# 数据库索引 |
||||
|
database: 0 |
||||
|
# 密码 |
||||
|
password: |
||||
|
# 连接超时时间 |
||||
|
timeout: 10s |
||||
|
lettuce: |
||||
|
pool: |
||||
|
# 连接池中的最小空闲连接 |
||||
|
min-idle: 0 |
||||
|
# 连接池中的最大空闲连接 |
||||
|
max-idle: 8 |
||||
|
# 连接池的最大数据库连接数 |
||||
|
max-active: 8 |
||||
|
# #连接池最大阻塞等待时间(使用负值表示没有限制) |
||||
|
max-wait: -1ms |
||||
|
|
||||
|
# token配置 |
||||
|
token: |
||||
|
# 令牌自定义标识 |
||||
|
header: Authorization |
||||
|
# 令牌密钥 |
||||
|
secret: abcdefghijklmnopqrstuvwxyz |
||||
|
# 令牌有效期(默认30分钟) |
||||
|
expireTime: 30 |
||||
|
|
||||
|
# MyBatis配置 |
||||
|
mybatis: |
||||
|
# 搜索指定包别名 |
||||
|
typeAliasesPackage: com.chenhai.**.domain |
||||
|
# 配置mapper的扫描,找到所有的mapper.xml映射文件 |
||||
|
mapperLocations: classpath*:mapper/**/*Mapper.xml |
||||
|
# 加载全局的配置文件 |
||||
|
configLocation: classpath:mybatis/mybatis-config.xml |
||||
|
|
||||
|
# PageHelper分页插件 |
||||
|
pagehelper: |
||||
|
helperDialect: mysql |
||||
|
supportMethodsArguments: true |
||||
|
params: count=countSql |
||||
|
|
||||
|
# Springdoc配置 |
||||
|
springdoc: |
||||
|
api-docs: |
||||
|
path: /v3/api-docs |
||||
|
swagger-ui: |
||||
|
enabled: true |
||||
|
path: /swagger-ui.html |
||||
|
tags-sorter: alpha |
||||
|
group-configs: |
||||
|
- group: 'default' |
||||
|
display-name: '测试模块' |
||||
|
paths-to-match: '/**' |
||||
|
packages-to-scan: com.chenhai.web.controller.tool |
||||
|
|
||||
|
# 防盗链配置 |
||||
|
referer: |
||||
|
# 防盗链开关 |
||||
|
enabled: false |
||||
|
# 允许的域名列表 |
||||
|
allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip |
||||
|
|
||||
|
# 防止XSS攻击 |
||||
|
xss: |
||||
|
# 过滤开关 |
||||
|
enabled: true |
||||
|
# 排除链接(多个用逗号分隔) |
||||
|
excludes: /system/notice |
||||
|
# 匹配链接 |
||||
|
urlPatterns: /system/*,/monitor/*,/tool/* |
||||
@ -0,0 +1,24 @@ |
|||||
|
Application Version: ${ruoyi.version} |
||||
|
Spring Boot Version: ${spring-boot.version} |
||||
|
//////////////////////////////////////////////////////////////////// |
||||
|
// _ooOoo_ // |
||||
|
// o8888888o // |
||||
|
// 88" . "88 // |
||||
|
// (| ^_^ |) // |
||||
|
// O\ = /O // |
||||
|
// ____/`---'\____ // |
||||
|
// .' \\| |// `. // |
||||
|
// / \\||| : |||// \ // |
||||
|
// / _||||| -:- |||||- \ // |
||||
|
// | | \\\ - /// | | // |
||||
|
// | \_| ''\---/'' | | // |
||||
|
// \ .-\__ `-` ___/-. / // |
||||
|
// ___`. .' /--.--\ `. . ___ // |
||||
|
// ."" '< `.___\_<|>_/___.' >'"". // |
||||
|
// | | : `- \`.;`\ _ /`;.`/ - ` : | | // |
||||
|
// \ \ `-. \_ __\ /__ _/ .-` / / // |
||||
|
// ========`-.____`-.___\_____/___.-`____.-'======== // |
||||
|
// `=---=' // |
||||
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // |
||||
|
// 佛祖保佑 永不宕机 永无BUG // |
||||
|
//////////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,38 @@ |
|||||
|
#错误消息 |
||||
|
not.null=* 必须填写 |
||||
|
user.jcaptcha.error=验证码错误 |
||||
|
user.jcaptcha.expire=验证码已失效 |
||||
|
user.not.exists=用户不存在/密码错误 |
||||
|
user.password.not.match=用户不存在/密码错误 |
||||
|
user.password.retry.limit.count=密码输入错误{0}次 |
||||
|
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 |
||||
|
user.password.delete=对不起,您的账号已被删除 |
||||
|
user.blocked=用户已封禁,请联系管理员 |
||||
|
role.blocked=角色已封禁,请联系管理员 |
||||
|
login.blocked=很遗憾,访问IP已被列入系统黑名单 |
||||
|
user.logout.success=退出成功 |
||||
|
|
||||
|
length.not.valid=长度必须在{min}到{max}个字符之间 |
||||
|
|
||||
|
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 |
||||
|
user.password.not.valid=* 5-50个字符 |
||||
|
|
||||
|
user.email.not.valid=邮箱格式错误 |
||||
|
user.mobile.phone.number.not.valid=手机号格式错误 |
||||
|
user.login.success=登录成功 |
||||
|
user.register.success=注册成功 |
||||
|
user.notfound=请重新登录 |
||||
|
user.forcelogout=管理员强制退出,请重新登录 |
||||
|
user.unknown.error=未知错误,请重新登录 |
||||
|
|
||||
|
##文件上传消息 |
||||
|
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB! |
||||
|
upload.filename.exceed.length=上传的文件名最长{0}个字符 |
||||
|
|
||||
|
##权限 |
||||
|
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] |
||||
|
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] |
||||
|
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] |
||||
|
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] |
||||
|
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] |
||||
|
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] |
||||
@ -0,0 +1,93 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<configuration> |
||||
|
<!-- 日志存放路径 --> |
||||
|
<property name="log.path" value="/home/ruoyi/logs" /> |
||||
|
<!-- 日志输出格式 --> |
||||
|
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" /> |
||||
|
|
||||
|
<!-- 控制台输出 --> |
||||
|
<appender name="console" class="ch.qos.logback.core.ConsoleAppender"> |
||||
|
<encoder> |
||||
|
<pattern>${log.pattern}</pattern> |
||||
|
</encoder> |
||||
|
</appender> |
||||
|
|
||||
|
<!-- 系统日志输出 --> |
||||
|
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
||||
|
<file>${log.path}/sys-info.log</file> |
||||
|
<!-- 循环政策:基于时间创建日志文件 --> |
||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> |
||||
|
<!-- 日志文件名格式 --> |
||||
|
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern> |
||||
|
<!-- 日志最大的历史 60天 --> |
||||
|
<maxHistory>60</maxHistory> |
||||
|
</rollingPolicy> |
||||
|
<encoder> |
||||
|
<pattern>${log.pattern}</pattern> |
||||
|
</encoder> |
||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter"> |
||||
|
<!-- 过滤的级别 --> |
||||
|
<level>INFO</level> |
||||
|
<!-- 匹配时的操作:接收(记录) --> |
||||
|
<onMatch>ACCEPT</onMatch> |
||||
|
<!-- 不匹配时的操作:拒绝(不记录) --> |
||||
|
<onMismatch>DENY</onMismatch> |
||||
|
</filter> |
||||
|
</appender> |
||||
|
|
||||
|
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
||||
|
<file>${log.path}/sys-error.log</file> |
||||
|
<!-- 循环政策:基于时间创建日志文件 --> |
||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> |
||||
|
<!-- 日志文件名格式 --> |
||||
|
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern> |
||||
|
<!-- 日志最大的历史 60天 --> |
||||
|
<maxHistory>60</maxHistory> |
||||
|
</rollingPolicy> |
||||
|
<encoder> |
||||
|
<pattern>${log.pattern}</pattern> |
||||
|
</encoder> |
||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter"> |
||||
|
<!-- 过滤的级别 --> |
||||
|
<level>ERROR</level> |
||||
|
<!-- 匹配时的操作:接收(记录) --> |
||||
|
<onMatch>ACCEPT</onMatch> |
||||
|
<!-- 不匹配时的操作:拒绝(不记录) --> |
||||
|
<onMismatch>DENY</onMismatch> |
||||
|
</filter> |
||||
|
</appender> |
||||
|
|
||||
|
<!-- 用户访问日志输出 --> |
||||
|
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
||||
|
<file>${log.path}/sys-user.log</file> |
||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> |
||||
|
<!-- 按天回滚 daily --> |
||||
|
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern> |
||||
|
<!-- 日志最大的历史 60天 --> |
||||
|
<maxHistory>60</maxHistory> |
||||
|
</rollingPolicy> |
||||
|
<encoder> |
||||
|
<pattern>${log.pattern}</pattern> |
||||
|
</encoder> |
||||
|
</appender> |
||||
|
|
||||
|
<!-- 系统模块日志级别控制 --> |
||||
|
<logger name="com.chenhai" level="info" /> |
||||
|
<!-- Spring日志级别控制 --> |
||||
|
<logger name="org.springframework" level="warn" /> |
||||
|
|
||||
|
<root level="info"> |
||||
|
<appender-ref ref="console" /> |
||||
|
</root> |
||||
|
|
||||
|
<!--系统操作日志--> |
||||
|
<root level="info"> |
||||
|
<appender-ref ref="file_info" /> |
||||
|
<appender-ref ref="file_error" /> |
||||
|
</root> |
||||
|
|
||||
|
<!--系统用户操作日志--> |
||||
|
<logger name="sys-user" level="info"> |
||||
|
<appender-ref ref="sys-user"/> |
||||
|
</logger> |
||||
|
</configuration> |
||||
@ -0,0 +1,20 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" ?> |
||||
|
<!DOCTYPE configuration |
||||
|
PUBLIC "-//mybatis.org//DTD Config 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-config.dtd"> |
||||
|
<configuration> |
||||
|
<!-- 全局参数 --> |
||||
|
<settings> |
||||
|
<!-- 使全局的映射器启用或禁用缓存 --> |
||||
|
<setting name="cacheEnabled" value="true" /> |
||||
|
<!-- 允许JDBC 支持自动生成主键 --> |
||||
|
<setting name="useGeneratedKeys" value="true" /> |
||||
|
<!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 --> |
||||
|
<setting name="defaultExecutorType" value="SIMPLE" /> |
||||
|
<!-- 指定 MyBatis 所用日志的具体实现 --> |
||||
|
<setting name="logImpl" value="SLF4J" /> |
||||
|
<!-- 使用驼峰命名法转换字段 --> |
||||
|
<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> --> |
||||
|
</settings> |
||||
|
|
||||
|
</configuration> |
||||
@ -0,0 +1,78 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
|
<parent> |
||||
|
<artifactId>chenhai</artifactId> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<version>3.9.0</version> |
||||
|
</parent> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
|
||||
|
<artifactId>chenhai-ai</artifactId> |
||||
|
|
||||
|
<description> |
||||
|
ai系统模块 |
||||
|
</description> |
||||
|
|
||||
|
<dependencies> |
||||
|
|
||||
|
<!-- 通用工具--> |
||||
|
<dependency> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<artifactId>chenhai-common</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 核心模块--> |
||||
|
<dependency> |
||||
|
<groupId>com.chenhai</groupId> |
||||
|
<artifactId>chenhai-framework</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-web</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.springframework.ai</groupId> |
||||
|
<artifactId>spring-ai-starter-model-zhipuai</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.springframework.ai</groupId> |
||||
|
<artifactId>spring-ai-starter-mcp-client</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.alibaba.cloud.ai</groupId> |
||||
|
<artifactId>spring-ai-alibaba-graph-core</artifactId> |
||||
|
<!-- <version>1.0.0.4</version>--> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.springframework.ai</groupId> |
||||
|
<artifactId>spring-ai-starter-model-ollama</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.projectlombok</groupId> |
||||
|
<artifactId>lombok</artifactId> |
||||
|
<scope>provided</scope> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.bitbucket.cowwoc</groupId> |
||||
|
<artifactId>diff-match-patch</artifactId> |
||||
|
<version>1.2</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.vladsch.flexmark</groupId> |
||||
|
<artifactId>flexmark-all</artifactId> |
||||
|
<version>0.64.8</version> |
||||
|
</dependency> |
||||
|
|
||||
|
</dependencies> |
||||
|
|
||||
|
</project> |
||||
@ -0,0 +1,43 @@ |
|||||
|
package com.chenhai.chenhaiai.config; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import org.springframework.core.task.TaskExecutor; |
||||
|
import org.springframework.core.task.support.TaskExecutorAdapter; |
||||
|
import org.springframework.scheduling.annotation.EnableAsync; |
||||
|
import org.springframework.scheduling.annotation.EnableScheduling; |
||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
||||
|
|
||||
|
import java.util.concurrent.Executor; |
||||
|
import java.util.concurrent.ThreadPoolExecutor; |
||||
|
|
||||
|
@Configuration |
||||
|
@EnableAsync |
||||
|
@EnableScheduling |
||||
|
public class AsyncConfig { |
||||
|
|
||||
|
@Value("${gitea.analysis.timeout:60}") |
||||
|
private int timeoutSeconds; |
||||
|
|
||||
|
@Bean("giteaTaskExecutor") |
||||
|
public ThreadPoolTaskExecutor giteaTaskExecutor() { |
||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); |
||||
|
int coreCount = Runtime.getRuntime().availableProcessors(); |
||||
|
executor.setCorePoolSize(Math.max(4, coreCount)); |
||||
|
executor.setMaxPoolSize(coreCount * 2); |
||||
|
executor.setQueueCapacity(100); |
||||
|
executor.setThreadNamePrefix("gitea-async-"); |
||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); |
||||
|
executor.setWaitForTasksToCompleteOnShutdown(true); |
||||
|
executor.setAwaitTerminationSeconds(5); |
||||
|
executor.setKeepAliveSeconds(60); |
||||
|
executor.initialize(); |
||||
|
return executor; |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
public TaskExecutor taskExecutor() { |
||||
|
return new TaskExecutorAdapter(giteaTaskExecutor()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.chenhai.chenhaiai.config; |
||||
|
|
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.context.ApplicationContext; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
@Component |
||||
|
public class BeanChecker { |
||||
|
|
||||
|
@Autowired |
||||
|
private ApplicationContext context; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void checkBeans() { |
||||
|
System.out.println("=== 所有ChatClient Bean ==="); |
||||
|
String[] beanNames = context.getBeanNamesForType(org.springframework.ai.chat.client.ChatClient.class); |
||||
|
for (String name : beanNames) { |
||||
|
System.out.println("Bean名称: " + name); |
||||
|
} |
||||
|
|
||||
|
System.out.println("=== 所有ChatModel Bean ==="); |
||||
|
String[] modelNames = context.getBeanNamesForType(org.springframework.ai.chat.model.ChatModel.class); |
||||
|
for (String name : modelNames) { |
||||
|
System.out.println("Bean名称: " + name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
package com.chenhai.chenhaiai.config; |
||||
|
|
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.model.ChatModel; |
||||
|
import org.springframework.ai.ollama.OllamaChatModel; |
||||
|
import org.springframework.ai.tool.ToolCallback; |
||||
|
import org.springframework.ai.tool.ToolCallbackProvider; |
||||
|
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import org.springframework.context.annotation.Primary; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-05 14:45 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Configuration |
||||
|
public class ChatClientConfig { |
||||
|
@Bean("toolChatClient") |
||||
|
public ChatClient toolChatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) { |
||||
|
List<ToolCallback> toolCallbacks = List.of(toolCallbackProvider.getToolCallbacks()); |
||||
|
return builder |
||||
|
.defaultToolCallbacks(toolCallbacks) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 指定Ollama为主要模型 |
||||
|
*/ |
||||
|
@Bean |
||||
|
@Primary |
||||
|
public ChatModel primaryChatModel(OllamaChatModel ollamaChatModel) { |
||||
|
return ollamaChatModel; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,141 @@ |
|||||
|
package com.chenhai.chenhaiai.config; |
||||
|
|
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.ollama.api.OllamaChatOptions; |
||||
|
import org.springframework.ai.ollama.OllamaChatModel; |
||||
|
import org.springframework.ai.zhipuai.ZhiPuAiChatModel; |
||||
|
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; |
||||
|
import org.springframework.beans.factory.annotation.Qualifier; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.*; |
||||
|
|
||||
|
@Component |
||||
|
public class ChatModelFactory { |
||||
|
|
||||
|
private final ZhiPuAiChatModel zhiPuModel; |
||||
|
private final OllamaChatModel ollamaModel; |
||||
|
|
||||
|
// 核心:模型 -> 提供方 映射 |
||||
|
private final Map<String, String> modelProviderMap = new HashMap<>(); |
||||
|
private final List<ModelInfo> availableModels = new ArrayList<>(); |
||||
|
|
||||
|
public ChatModelFactory( |
||||
|
@Qualifier("zhiPuAiChatModel") ZhiPuAiChatModel zhiPuModel, |
||||
|
@Qualifier("ollamaChatModel") OllamaChatModel ollamaModel) { |
||||
|
|
||||
|
this.zhiPuModel = zhiPuModel; |
||||
|
this.ollamaModel = ollamaModel; |
||||
|
|
||||
|
initModelMappings(); |
||||
|
} |
||||
|
|
||||
|
private void initModelMappings() { |
||||
|
// 智谱AI模型 |
||||
|
addModel("glm-4-flash", "智谱GLM-4-Flash", "zhipu"); |
||||
|
addModel("glm-4", "智谱GLM-4", "zhipu"); |
||||
|
|
||||
|
// 🔥 Ollama模型 |
||||
|
addModel("qwen3:4b", "通义千问4B", "ollama"); |
||||
|
addModel("llama2", "Llama2 7B", "ollama"); |
||||
|
addModel("mistral", "Mistral 7B", "ollama"); |
||||
|
addModel("deepseek-r1:14b", "DeepSeek R1 14B", "ollama"); |
||||
|
addModel("gemma:7b", "Gemma 7B", "ollama"); |
||||
|
addModel("codellama", "CodeLlama", "ollama"); |
||||
|
} |
||||
|
|
||||
|
private void addModel(String value, String label, String provider) { |
||||
|
modelProviderMap.put(value, provider); |
||||
|
availableModels.add(new ModelInfo(value, label, provider)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取ChatClient - 根据映射关系选择 |
||||
|
*/ |
||||
|
public ChatClient getChatClient(String modelName) { |
||||
|
if (modelName == null || modelName.trim().isEmpty()) { |
||||
|
modelName = "glm-4-flash"; // 默认 |
||||
|
} |
||||
|
|
||||
|
String provider = modelProviderMap.get(modelName); |
||||
|
if (provider == null) { |
||||
|
// 没找到映射,默认用智谱 |
||||
|
provider = "zhipu"; |
||||
|
modelName = "glm-4-flash"; |
||||
|
} |
||||
|
|
||||
|
// 根据提供方返回对应的ChatClient |
||||
|
return switch (provider) { |
||||
|
case "zhipu" -> getZhiPuClient(); |
||||
|
case "ollama" -> getOllamaClient(modelName); |
||||
|
default -> getZhiPuClient(); // 默认智谱 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private ChatClient getZhiPuClient() { |
||||
|
return ChatClient.builder(zhiPuModel) |
||||
|
.defaultOptions(ZhiPuAiChatOptions.builder() |
||||
|
.model("glm-4-flash") |
||||
|
.topP(0.7) |
||||
|
.temperature(0.7) |
||||
|
.build()) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
private ChatClient getOllamaClient(String modelName) { |
||||
|
return ChatClient.builder(ollamaModel) |
||||
|
.defaultOptions(OllamaChatOptions.builder() |
||||
|
.model(modelName) // Ollama需要具体的模型名 |
||||
|
.temperature(0.7) |
||||
|
.topP(0.8) |
||||
|
.build()) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取可用模型列表 |
||||
|
*/ |
||||
|
public List<ModelInfo> getAvailableModels() { |
||||
|
return availableModels; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取默认模型 |
||||
|
*/ |
||||
|
public String getDefaultModel() { |
||||
|
return "qwen3:4b"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断模型是否可用 |
||||
|
*/ |
||||
|
public boolean isModelAvailable(String modelName) { |
||||
|
return modelProviderMap.containsKey(modelName); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取模型的提供方 |
||||
|
*/ |
||||
|
public String getModelProvider(String modelName) { |
||||
|
return modelProviderMap.getOrDefault(modelName, "zhipu"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 简单模型信息类 |
||||
|
*/ |
||||
|
public static class ModelInfo { |
||||
|
private final String value; |
||||
|
private final String label; |
||||
|
private final String provider; |
||||
|
|
||||
|
public ModelInfo(String value, String label, String provider) { |
||||
|
this.value = value; |
||||
|
this.label = label; |
||||
|
this.provider = provider; |
||||
|
} |
||||
|
|
||||
|
public String getValue() { return value; } |
||||
|
public String getLabel() { return label; } |
||||
|
public String getProvider() { return provider; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,152 @@ |
|||||
|
package com.chenhai.chenhaiai.config; |
||||
|
|
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.*; |
||||
|
import com.alibaba.cloud.ai.graph.action.AsyncEdgeAction; |
||||
|
import com.alibaba.cloud.ai.graph.action.AsyncNodeAction; |
||||
|
import com.alibaba.cloud.ai.graph.exception.GraphStateException; |
||||
|
import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.node.*; |
||||
|
import com.chenhai.chenhaiai.node.weekPlan.*; |
||||
|
import com.chenhai.chenhaiai.node.weekPlan.jdbc.*; |
||||
|
import com.chenhai.chenhaiai.node.weekPlan.mcp.DeptNode; |
||||
|
import com.chenhai.chenhaiai.node.weekPlan.mcp.UserNode; |
||||
|
import com.chenhai.chenhaiai.node.weekPlan.mcp.WeekPlanDetailNode; |
||||
|
import com.chenhai.chenhaiai.node.weekPlan.mcp.WeekPlanMainNode; |
||||
|
import com.chenhai.chenhaiai.service.GiteaAnalysisService; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.beans.factory.annotation.Qualifier; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Configuration |
||||
|
public class GraphConfig { |
||||
|
|
||||
|
|
||||
|
private static final Logger log = LoggerFactory.getLogger(GraphConfig.class); |
||||
|
|
||||
|
@Bean("weekPlanAnalysisNode") |
||||
|
public CompiledGraph simpleGraph(ChatClient.Builder clientBuilder) throws GraphStateException { |
||||
|
KeyStrategyFactory keyStrategyFactory = () -> Map.of("weekPlan",new ReplaceStrategy(),"personalDaily",new ReplaceStrategy()); |
||||
|
// 创建状态图 |
||||
|
StateGraph stateGraph = new StateGraph("WeekPlanAnalysisNode",keyStrategyFactory); |
||||
|
// 添加节点 |
||||
|
stateGraph.addNode("DataTranslationNode", AsyncNodeAction.node_async(new DataTranslationNode(clientBuilder))); |
||||
|
stateGraph.addNode("DataOrganizationNode", AsyncNodeAction.node_async(new DataOrganizationNode(clientBuilder))); |
||||
|
stateGraph.addNode("DataAssociationNode", AsyncNodeAction.node_async(new DataAssociationNode(clientBuilder))); |
||||
|
stateGraph.addNode("WeekPlanAnalysisNode", AsyncNodeAction.node_async(new WeekPlanAnalysisNode(clientBuilder))); |
||||
|
// 定义边 |
||||
|
stateGraph.addEdge(StateGraph.START,"DataTranslationNode"); |
||||
|
stateGraph.addEdge("DataTranslationNode","DataOrganizationNode"); |
||||
|
stateGraph.addEdge("DataOrganizationNode","DataAssociationNode"); |
||||
|
stateGraph.addEdge("DataAssociationNode","WeekPlanAnalysisNode"); |
||||
|
stateGraph.addEdge("WeekPlanAnalysisNode",StateGraph.END); |
||||
|
|
||||
|
// 编译状态图 放入容器 |
||||
|
return stateGraph.compile(); |
||||
|
} |
||||
|
|
||||
|
@Bean("weekPlanNode") |
||||
|
public CompiledGraph weekPlanNodeGraph(@Qualifier("toolChatClient") ChatClient chatClient, ProgressEmitter progressEmitter) throws GraphStateException { |
||||
|
KeyStrategyFactory keyStrategyFactory = () -> Map.of("weekPlanResponse",new ReplaceStrategy()); |
||||
|
// 创建状态图 |
||||
|
StateGraph stateGraph = new StateGraph("weekPlanNode",keyStrategyFactory); |
||||
|
// 添加节点 |
||||
|
stateGraph.addNode("DeptNode", AsyncNodeAction.node_async(new DeptNode(chatClient))); |
||||
|
stateGraph.addNode("UserNode", AsyncNodeAction.node_async(new UserNode(chatClient))); |
||||
|
stateGraph.addNode("WeekPlanMainNode", AsyncNodeAction.node_async(new WeekPlanMainNode(chatClient))); |
||||
|
stateGraph.addNode("WeekPlanDetailNode", AsyncNodeAction.node_async(new WeekPlanDetailNode(chatClient))); |
||||
|
stateGraph.addNode("DailyPaperNode", AsyncNodeAction.node_async(new DailyPaperJdbcNode(progressEmitter))); |
||||
|
// stateGraph.addNode("DailyPaperNode", AsyncNodeAction.node_async(new DailyPaperNode(chatClient))); |
||||
|
stateGraph.addNode("Analysis", AsyncNodeAction.node_async(new Analysis(chatClient))); |
||||
|
// 定义边 |
||||
|
stateGraph.addEdge(StateGraph.START,"DeptNode"); |
||||
|
stateGraph.addEdge("DeptNode","UserNode"); |
||||
|
stateGraph.addEdge("UserNode","WeekPlanMainNode"); |
||||
|
stateGraph.addEdge("WeekPlanMainNode","WeekPlanDetailNode"); |
||||
|
stateGraph.addEdge("WeekPlanDetailNode","DailyPaperNode"); |
||||
|
stateGraph.addEdge("DailyPaperNode","Analysis"); |
||||
|
stateGraph.addEdge("Analysis",StateGraph.END); |
||||
|
|
||||
|
// 编译状态图 放入容器 |
||||
|
return stateGraph.compile(); |
||||
|
} |
||||
|
|
||||
|
// @Bean("weekPlanNodeJdbcGraph") |
||||
|
// public CompiledGraph weekPlanNodeStream(ChatClient chatClient) throws GraphStateException { |
||||
|
// KeyStrategyFactory keyStrategyFactory = () -> Map.of("weekPlanResponse",new ReplaceStrategy()); |
||||
|
// // 创建状态图 |
||||
|
// StateGraph stateGraph = new StateGraph("weekPlanNodeJdbcGraph",keyStrategyFactory); |
||||
|
// // 添加节点 |
||||
|
// stateGraph.addNode("DeptJdbcNode", AsyncNodeAction.node_async(new DeptJdbcNode())); |
||||
|
// stateGraph.addNode("UserJdbcNode", AsyncNodeAction.node_async(new UserJdbcNode())); |
||||
|
// stateGraph.addNode("WeekPlanMainJdbcNode", AsyncNodeAction.node_async(new WeekPlanMainJdbcNode())); |
||||
|
// stateGraph.addNode("WeekPlanDetailJdbcNode", AsyncNodeAction.node_async(new WeekPlanDetailJdbcNode())); |
||||
|
// stateGraph.addNode("DailyPaperJdbcNode", AsyncNodeAction.node_async(new DailyPaperJdbcNode())); |
||||
|
// // 定义边 |
||||
|
// stateGraph.addEdge(StateGraph.START,"DeptJdbcNode"); |
||||
|
// stateGraph.addEdge("DeptJdbcNode","UserJdbcNode"); |
||||
|
// stateGraph.addEdge("UserJdbcNode","WeekPlanMainJdbcNode"); |
||||
|
// stateGraph.addEdge("WeekPlanMainJdbcNode","WeekPlanDetailJdbcNode"); |
||||
|
// stateGraph.addEdge("WeekPlanDetailJdbcNode","DailyPaperJdbcNode"); |
||||
|
// stateGraph.addEdge("DailyPaperJdbcNode",StateGraph.END); |
||||
|
// |
||||
|
// // 编译状态图 放入容器 |
||||
|
// return stateGraph.compile(); |
||||
|
// } |
||||
|
|
||||
|
@Bean("weekPlanNodeJdbcGraph") |
||||
|
public CompiledGraph weekPlanNodeStream(ProgressEmitter progressEmitter, GiteaAnalysisService giteaAnalysisService) throws GraphStateException { |
||||
|
KeyStrategyFactory keyStrategyFactory = () -> Map.of( |
||||
|
"weekPlanResponse", new ReplaceStrategy(), |
||||
|
"isResearchDept", new ReplaceStrategy() |
||||
|
); |
||||
|
|
||||
|
StateGraph stateGraph = new StateGraph("weekPlanNodeJdbcGraph", keyStrategyFactory); |
||||
|
|
||||
|
// 添加节点(新增GitAnalysisNode) |
||||
|
stateGraph.addNode("DeptJdbcNode", AsyncNodeAction.node_async(new DeptJdbcNode(progressEmitter))); |
||||
|
stateGraph.addNode("UserJdbcNode", AsyncNodeAction.node_async(new UserJdbcNode(progressEmitter))); |
||||
|
stateGraph.addNode("WeekPlanMainJdbcNode", AsyncNodeAction.node_async(new WeekPlanMainJdbcNode(progressEmitter))); |
||||
|
stateGraph.addNode("WeekPlanDetailJdbcNode", AsyncNodeAction.node_async(new WeekPlanDetailJdbcNode(progressEmitter))); |
||||
|
stateGraph.addNode("DailyPaperJdbcNode", AsyncNodeAction.node_async(new DailyPaperJdbcNode(progressEmitter))); |
||||
|
stateGraph.addNode("GitAnalysisNode", AsyncNodeAction.node_async(new GitAnalysisNode(progressEmitter, giteaAnalysisService))); |
||||
|
|
||||
|
// 定义边 - 关键修改点(3处) |
||||
|
stateGraph.addEdge(StateGraph.START, "DeptJdbcNode"); |
||||
|
stateGraph.addEdge("DeptJdbcNode", "UserJdbcNode"); |
||||
|
stateGraph.addEdge("UserJdbcNode", "WeekPlanMainJdbcNode"); |
||||
|
|
||||
|
// 修改点1:WeekPlanMainJdbcNode后,根据是否为研发部分支 |
||||
|
stateGraph.addConditionalEdges("WeekPlanMainJdbcNode", |
||||
|
AsyncEdgeAction.edge_async( |
||||
|
state -> { |
||||
|
// 简单判断:是研发部就走"yes",否则走"no" |
||||
|
Boolean isResearchDept = state.value("isResearchDept", false); |
||||
|
return isResearchDept ? "yes" : "no"; |
||||
|
} |
||||
|
), |
||||
|
Map.of( |
||||
|
"yes", "GitAnalysisNode", // 研发部:先执行Git分析 |
||||
|
"no", "WeekPlanDetailJdbcNode" // 其他部门:直接获取周计划详情 |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
// 修改点2:Git分析完成后,再获取周计划详情 |
||||
|
stateGraph.addEdge("GitAnalysisNode", "WeekPlanDetailJdbcNode"); |
||||
|
|
||||
|
// 修改点3:周计划详情获取完成后,获取日报 |
||||
|
stateGraph.addEdge("WeekPlanDetailJdbcNode", "DailyPaperJdbcNode"); |
||||
|
|
||||
|
// 日报节点后结束 |
||||
|
stateGraph.addEdge("DailyPaperJdbcNode", StateGraph.END); |
||||
|
|
||||
|
return stateGraph.compile(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
package com.chenhai.chenhaiai.config; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-11 9:50 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Configuration |
||||
|
public class ProgressEmitterConfig { |
||||
|
@Bean |
||||
|
public ProgressEmitter progressEmitter() { |
||||
|
return new ProgressEmitter(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
package com.chenhai.chenhaiai.controller; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.service.gitNew.GiteaGranularityService; |
||||
|
import com.chenhai.chenhaiai.service.gitNew.GiteaQueryService; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestParam; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.time.temporal.ChronoUnit; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Slf4j |
||||
|
@RestController |
||||
|
@RequestMapping("/gitea") |
||||
|
public class GiteaController { |
||||
|
|
||||
|
|
||||
|
@Autowired |
||||
|
private GiteaQueryService giteaQueryService; |
||||
|
|
||||
|
@Autowired |
||||
|
private GiteaGranularityService giteaGranularityService; |
||||
|
|
||||
|
/** |
||||
|
* 获取文本分析报告 - 测试专用(固定时间范围) |
||||
|
*/ |
||||
|
@GetMapping("/analysis/text") |
||||
|
public Map<String, Object> getTextAnalysisReport() { |
||||
|
// 固定时间范围:只传年月日 |
||||
|
String since = "2024-01-01"; |
||||
|
String until = "2024-12-30"; |
||||
|
|
||||
|
System.out.println("使用固定时间范围: " + since + " 至 " + until); |
||||
|
|
||||
|
return giteaQueryService.getTextAnalysisReport(since, until); |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/analysis/text/granular") |
||||
|
public Map<String, Object> getTextAnalysisReportWithGranularity( |
||||
|
@RequestParam(value = "since", defaultValue = "2024-01-01") String since, |
||||
|
@RequestParam(value = "until", defaultValue = "2024-01-31") String until, |
||||
|
@RequestParam(value = "granularity", defaultValue = "auto") String granularity) { |
||||
|
|
||||
|
// 自动限制时间范围,防止数据过大 |
||||
|
LocalDate sinceDate = LocalDate.parse(since); |
||||
|
LocalDate untilDate = LocalDate.parse(until); |
||||
|
long days = ChronoUnit.DAYS.between(sinceDate, untilDate); |
||||
|
|
||||
|
// 根据颗粒度限制时间范围 |
||||
|
switch (granularity.toLowerCase()) { |
||||
|
case "day": |
||||
|
if (days > 30) { |
||||
|
untilDate = sinceDate.plusDays(30); |
||||
|
until = untilDate.toString(); |
||||
|
log.warn("day颗粒度限制30天,自动调整为: {} 至 {}", since, until); |
||||
|
} |
||||
|
break; |
||||
|
case "week": |
||||
|
if (days > 90) { |
||||
|
untilDate = sinceDate.plusDays(90); |
||||
|
until = untilDate.toString(); |
||||
|
log.warn("week颗粒度限制90天,自动调整为: {} 至 {}", since, until); |
||||
|
} |
||||
|
break; |
||||
|
case "month": |
||||
|
if (days > 730) { // 2年 |
||||
|
untilDate = sinceDate.plusDays(730); |
||||
|
until = untilDate.toString(); |
||||
|
log.warn("month颗粒度限制2年,自动调整为: {} 至 {}", since, until); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
System.out.println("使用时间范围: " + since + " 至 " + until + ",颗粒度: " + granularity); |
||||
|
|
||||
|
return giteaGranularityService.getTextAnalysisReportWithGranularity(since, until, granularity); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,526 @@ |
|||||
|
package com.chenhai.chenhaiai.controller; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.CompiledGraph; |
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.chenhai.chenhaiai.config.ChatModelFactory; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.service.MarkdownService; |
||||
|
import com.chenhai.chenhaiai.utils.CharacterStreamProcessor; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import com.chenhai.chenhaiai.utils.PromptLoader; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.beans.factory.annotation.Qualifier; |
||||
|
import org.springframework.http.MediaType; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
import reactor.core.publisher.Flux; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
import java.util.concurrent.CompletableFuture; |
||||
|
|
||||
|
@RestController |
||||
|
@RequestMapping("/graph") |
||||
|
public class GraphController { |
||||
|
|
||||
|
private final CompiledGraph weekPlanNodeJdbcGraph; |
||||
|
// private final ChatClient chatClient; // 原本的模型不分哪一个,系统直接用配置好的 |
||||
|
|
||||
|
// 现在区分什么模型 |
||||
|
private final ChatModelFactory modelFactory; |
||||
|
private final PromptLoader promptLoader; |
||||
|
|
||||
|
private final ProgressEmitter progressEmitter; |
||||
|
|
||||
|
private final MarkdownService markdownService; |
||||
|
private final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
public GraphController( |
||||
|
@Qualifier("weekPlanNodeJdbcGraph") CompiledGraph weekPlanNodeJdbcGraph, |
||||
|
// ChatClient chatClient, |
||||
|
ChatModelFactory modelFactory, |
||||
|
PromptLoader promptLoader, |
||||
|
ProgressEmitter progressEmitter, |
||||
|
MarkdownService markdownService) { |
||||
|
this.weekPlanNodeJdbcGraph = weekPlanNodeJdbcGraph; |
||||
|
// this.chatClient = chatClient; |
||||
|
this.modelFactory = modelFactory; |
||||
|
this.promptLoader = promptLoader; |
||||
|
this.progressEmitter = progressEmitter; |
||||
|
this.markdownService = markdownService; |
||||
|
} |
||||
|
|
||||
|
@GetMapping(value = "/weekPlanAnalysisStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
||||
|
public Flux<String> weekPlanAnalysisStream( |
||||
|
@RequestParam("deptName") String deptName, |
||||
|
@RequestParam("weekDisplay") String weekDisplay, |
||||
|
@RequestParam(value = "model", required = false) String model) { |
||||
|
|
||||
|
return Flux.create(sink -> { |
||||
|
try { |
||||
|
// 设置进度推送器 |
||||
|
progressEmitter.setSink(sink); |
||||
|
|
||||
|
// 🔥 立即发送开始状态(确保前端立即看到) |
||||
|
String startMsg = CharacterStreamProcessor.formatMessage("status", |
||||
|
"🚀 启动分析流程 - " + deptName + " " + weekDisplay); |
||||
|
sink.next(startMsg); |
||||
|
|
||||
|
// 给前端一点时间显示 |
||||
|
Thread.sleep(100); |
||||
|
|
||||
|
// 准备请求数据 |
||||
|
WeekPlanResponse weekPlanResponse = new WeekPlanResponse(); |
||||
|
weekPlanResponse.setDeptName(deptName); |
||||
|
weekPlanResponse.setWeekDisplay(weekDisplay); |
||||
|
|
||||
|
// 🔥 使用真正的异步执行Graph |
||||
|
CompletableFuture.supplyAsync(() -> { |
||||
|
try { |
||||
|
System.out.println("开始执行Graph节点..."); |
||||
|
|
||||
|
// 执行Graph - 这会触发各个节点的progressEmitter调用 |
||||
|
Optional<OverAllState> stateOptional = weekPlanNodeJdbcGraph.invoke( |
||||
|
Map.of("weekPlanResponse", weekPlanResponse) |
||||
|
); |
||||
|
|
||||
|
if (stateOptional.isEmpty()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", "未获取到数据")); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 提取完整数据 |
||||
|
Map<String, Object> stateData = stateOptional.get().data(); |
||||
|
Object rawData = stateData.get("weekPlanResponse"); |
||||
|
String jsonStr = objectMapper.writeValueAsString(rawData); |
||||
|
return objectMapper.readValue(jsonStr, WeekPlanResponse.class); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"Graph执行错误: " + e.getMessage())); |
||||
|
e.printStackTrace(); |
||||
|
return null; |
||||
|
} |
||||
|
}, CompletableFuture.delayedExecutor(500, java.util.concurrent.TimeUnit.MILLISECONDS)) |
||||
|
.thenAccept(fullData -> { |
||||
|
if (fullData != null) { |
||||
|
try { |
||||
|
// 数据获取完成 |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("status", |
||||
|
"📊 数据获取完成,开始AI分析")); |
||||
|
|
||||
|
Thread.sleep(200); // 给用户看到状态 |
||||
|
|
||||
|
// 准备分析提示词 |
||||
|
String promptTemplate = promptLoader.loadPrompt("prompts/week_plan_analysis4.txt"); |
||||
|
String jsonData = objectMapper.writeValueAsString(fullData); |
||||
|
String finalPrompt = promptTemplate.replace("{jsonData}", jsonData); |
||||
|
|
||||
|
// 优化提示词用于流式输出 |
||||
|
finalPrompt = CharacterStreamProcessor.optimizePromptForStreaming(finalPrompt); |
||||
|
|
||||
|
ChatClient selectedClient = modelFactory.getChatClient(model); |
||||
|
// 启动真正的字符级流式处理 |
||||
|
Flux<String> characterStream = CharacterStreamProcessor.createCharacterStream( |
||||
|
selectedClient, |
||||
|
finalPrompt, |
||||
|
sink |
||||
|
); |
||||
|
|
||||
|
// 订阅流 |
||||
|
characterStream.subscribe( |
||||
|
chunk -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(chunk); |
||||
|
} |
||||
|
}, |
||||
|
error -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.error(error); |
||||
|
} |
||||
|
}, |
||||
|
() -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("complete", "✅ 分析完成")); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"AI分析错误: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"系统错误: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@GetMapping("/simpleGraph") |
||||
|
public Map<String, Object> simpleGraph( |
||||
|
@RequestParam("deptName") String deptName, |
||||
|
@RequestParam("weekDisplay") String weekDisplay){ |
||||
|
// 1. 准备请求数据 |
||||
|
WeekPlanResponse weekPlanResponse = new WeekPlanResponse(); |
||||
|
weekPlanResponse.setDeptName(deptName); |
||||
|
weekPlanResponse.setWeekDisplay(weekDisplay); |
||||
|
|
||||
|
// 2. 获取图节点数据 |
||||
|
Optional<OverAllState> stateOptional = weekPlanNodeJdbcGraph.invoke( |
||||
|
Map.of("weekPlanResponse", weekPlanResponse) |
||||
|
); |
||||
|
Map<String, Object> data = stateOptional.map(OverAllState::data).orElse(Map.of()); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/fullMarkdown") |
||||
|
public String fullMarkdown( |
||||
|
@RequestParam("deptName") String deptName, |
||||
|
@RequestParam("weekDisplay") String weekDisplay, |
||||
|
@RequestParam(value = "model", required = false) String model) { |
||||
|
|
||||
|
try { |
||||
|
// 获取完整数据 |
||||
|
Map<String, Object> rawData = simpleGraph(deptName, weekDisplay); |
||||
|
Object weekPlanObj = rawData.get("weekPlanResponse"); |
||||
|
|
||||
|
if (weekPlanObj == null) return "# 无数据"; |
||||
|
|
||||
|
// 转换为JSON字符串 |
||||
|
String jsonStr = objectMapper.writeValueAsString(weekPlanObj); |
||||
|
|
||||
|
// 原样全部转换 |
||||
|
return markdownService.fullJsonToMarkdown(jsonStr); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
return "# 错误\n\n" + e.getMessage(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@GetMapping(value = "/weekPlanAnalysisFromMarkdown", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
||||
|
public Flux<String> weekPlanAnalysisFromMarkdown( |
||||
|
@RequestParam("deptName") String deptName, |
||||
|
@RequestParam("weekDisplay") String weekDisplay, |
||||
|
@RequestParam(value = "model", required = false) String model) { |
||||
|
|
||||
|
return Flux.create(sink -> { |
||||
|
try { |
||||
|
// 设置进度推送器 |
||||
|
progressEmitter.setSink(sink); |
||||
|
|
||||
|
// 🔥 立即发送开始状态 |
||||
|
String startMsg = CharacterStreamProcessor.formatMessage("status", |
||||
|
"🚀 启动Markdown格式分析流程 - " + deptName + " " + weekDisplay); |
||||
|
sink.next(startMsg); |
||||
|
|
||||
|
// 给前端一点时间显示 |
||||
|
Thread.sleep(100); |
||||
|
|
||||
|
// 准备请求数据 |
||||
|
WeekPlanResponse weekPlanResponse = new WeekPlanResponse(); |
||||
|
weekPlanResponse.setDeptName(deptName); |
||||
|
weekPlanResponse.setWeekDisplay(weekDisplay); |
||||
|
|
||||
|
// 🔥 使用真正的异步执行Graph |
||||
|
CompletableFuture.supplyAsync(() -> { |
||||
|
try { |
||||
|
System.out.println("开始执行Graph节点(Markdown模式)..."); |
||||
|
|
||||
|
// 执行Graph - 这会触发各个节点的progressEmitter调用 |
||||
|
Optional<OverAllState> stateOptional = weekPlanNodeJdbcGraph.invoke( |
||||
|
Map.of("weekPlanResponse", weekPlanResponse) |
||||
|
); |
||||
|
|
||||
|
if (stateOptional.isEmpty()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", "未获取到数据")); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 提取完整数据并转换为Markdown |
||||
|
Map<String, Object> stateData = stateOptional.get().data(); |
||||
|
Object rawData = stateData.get("weekPlanResponse"); |
||||
|
String jsonStr = objectMapper.writeValueAsString(rawData); |
||||
|
|
||||
|
// 转换为Markdown格式 |
||||
|
String markdownContent = markdownService.fullJsonToMarkdown(jsonStr); |
||||
|
|
||||
|
return markdownContent; |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"Graph执行或Markdown转换错误: " + e.getMessage())); |
||||
|
e.printStackTrace(); |
||||
|
return null; |
||||
|
} |
||||
|
}, CompletableFuture.delayedExecutor(500, java.util.concurrent.TimeUnit.MILLISECONDS)) |
||||
|
.thenAccept(markdownContent -> { |
||||
|
if (markdownContent != null) { |
||||
|
try { |
||||
|
// 数据获取完成 |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("status", |
||||
|
"📝 Markdown格式数据准备完成,开始AI分析")); |
||||
|
|
||||
|
Thread.sleep(200); // 给用户看到状态 |
||||
|
|
||||
|
// 准备专门针对Markdown的分析提示词(使用原有的提示词模板) |
||||
|
String promptTemplate = promptLoader.loadPrompt("prompts/week_plan_analysis4.txt"); |
||||
|
|
||||
|
// 构建提示词:只传入Markdown内容 |
||||
|
// 注意:这里我们将Markdown内容放到{jsonData}占位符中,提示词可能需要稍作调整 |
||||
|
String finalPrompt = promptTemplate.replace("{jsonData}", |
||||
|
"以下是数据的Markdown格式表示:\n\n" + markdownContent); |
||||
|
|
||||
|
// 优化提示词用于流式输出 |
||||
|
finalPrompt = CharacterStreamProcessor.optimizePromptForStreaming(finalPrompt); |
||||
|
|
||||
|
// 🔥 使用工厂获取客户端 |
||||
|
ChatClient selectedClient = modelFactory.getChatClient(model); |
||||
|
// 启动真正的字符级流式处理 |
||||
|
Flux<String> characterStream = CharacterStreamProcessor.createCharacterStream( |
||||
|
selectedClient, |
||||
|
finalPrompt, |
||||
|
sink |
||||
|
); |
||||
|
|
||||
|
// 订阅流 |
||||
|
characterStream.subscribe( |
||||
|
chunk -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(chunk); |
||||
|
} |
||||
|
}, |
||||
|
error -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.error(error); |
||||
|
} |
||||
|
}, |
||||
|
() -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("complete", "✅ Markdown格式分析完成")); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"AI分析错误: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"系统错误: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 多视角周计划分析接口 |
||||
|
* |
||||
|
* @param deptName 部门名称 |
||||
|
* @param weekDisplay 周次显示 |
||||
|
* @param perspective 分析视角:management(管理)/process(流程)/culture(文化)/comprehensive(综合) |
||||
|
* @return SSE流 |
||||
|
*/ |
||||
|
@GetMapping(value = "/multiPerspectiveAnalysis", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
||||
|
public Flux<String> multiPerspectiveAnalysis( |
||||
|
@RequestParam("deptName") String deptName, |
||||
|
@RequestParam("weekDisplay") String weekDisplay, |
||||
|
@RequestParam(value = "perspective", defaultValue = "management") String perspective, |
||||
|
@RequestParam(value = "model", required = false) String model) { |
||||
|
|
||||
|
return Flux.create(sink -> { |
||||
|
try { |
||||
|
// 设置进度推送器 |
||||
|
progressEmitter.setSink(sink); |
||||
|
|
||||
|
// 根据视角显示不同的提示 |
||||
|
String perspectiveName = getPerspectiveName(perspective); |
||||
|
String startMsg = CharacterStreamProcessor.formatMessage("status", |
||||
|
"🚀 启动" + perspectiveName + "分析 - " + deptName + " " + weekDisplay); |
||||
|
sink.next(startMsg); |
||||
|
|
||||
|
Thread.sleep(100); |
||||
|
|
||||
|
// 准备请求数据 |
||||
|
WeekPlanResponse weekPlanResponse = new WeekPlanResponse(); |
||||
|
weekPlanResponse.setDeptName(deptName); |
||||
|
weekPlanResponse.setWeekDisplay(weekDisplay); |
||||
|
|
||||
|
// 异步执行Graph获取数据 |
||||
|
CompletableFuture.supplyAsync(() -> { |
||||
|
try { |
||||
|
System.out.println("开始执行Graph节点..."); |
||||
|
|
||||
|
// 执行Graph获取数据 |
||||
|
Optional<OverAllState> stateOptional = weekPlanNodeJdbcGraph.invoke( |
||||
|
Map.of("weekPlanResponse", weekPlanResponse) |
||||
|
); |
||||
|
|
||||
|
if (stateOptional.isEmpty()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", "未获取到数据")); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 提取数据并转换为JSON |
||||
|
Map<String, Object> stateData = stateOptional.get().data(); |
||||
|
Object rawData = stateData.get("weekPlanResponse"); |
||||
|
String jsonStr = objectMapper.writeValueAsString(rawData); |
||||
|
|
||||
|
// 同时保留JSON和Markdown格式 |
||||
|
Map<String, String> dataFormats = new HashMap<>(); |
||||
|
dataFormats.put("json", jsonStr); |
||||
|
dataFormats.put("markdown", markdownService.fullJsonToMarkdown(jsonStr)); |
||||
|
|
||||
|
return dataFormats; |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"数据获取错误: " + e.getMessage())); |
||||
|
e.printStackTrace(); |
||||
|
return null; |
||||
|
} |
||||
|
}, CompletableFuture.delayedExecutor(500, java.util.concurrent.TimeUnit.MILLISECONDS)) |
||||
|
.thenAccept(dataFormats -> { |
||||
|
if (dataFormats != null) { |
||||
|
try { |
||||
|
// 数据获取完成 |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("status", |
||||
|
"📊 数据准备完成,开始" + getPerspectiveName(perspective) + "分析")); |
||||
|
|
||||
|
Thread.sleep(200); |
||||
|
|
||||
|
// 根据视角加载不同的提示词模板 |
||||
|
String promptTemplate = loadPerspectivePrompt(perspective); |
||||
|
|
||||
|
// 准备数据:JSON用于结构化分析,Markdown用于文本分析 |
||||
|
String analysisData = ""; |
||||
|
if (perspective.equals("management") || perspective.equals("comprehensive")) { |
||||
|
// 管理视角和综合视角使用JSON格式 |
||||
|
analysisData = dataFormats.get("json"); |
||||
|
} else { |
||||
|
// 流程和文化视角可以使用Markdown格式 |
||||
|
analysisData = dataFormats.get("markdown"); |
||||
|
} |
||||
|
|
||||
|
// 构建完整提示词 |
||||
|
String finalPrompt = buildPerspectivePrompt(promptTemplate, analysisData, |
||||
|
deptName, weekDisplay, perspective); |
||||
|
|
||||
|
// 优化提示词用于流式输出 |
||||
|
finalPrompt = CharacterStreamProcessor.optimizePromptForStreaming(finalPrompt); |
||||
|
|
||||
|
// 🔥 使用工厂获取客户端 |
||||
|
ChatClient selectedClient = modelFactory.getChatClient(model); |
||||
|
// 启动字符级流式处理 |
||||
|
Flux<String> characterStream = CharacterStreamProcessor.createCharacterStream( |
||||
|
selectedClient, |
||||
|
finalPrompt, |
||||
|
sink |
||||
|
); |
||||
|
|
||||
|
// 订阅流 |
||||
|
characterStream.subscribe( |
||||
|
chunk -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(chunk); |
||||
|
} |
||||
|
}, |
||||
|
error -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"AI分析错误: " + error.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
}, |
||||
|
() -> { |
||||
|
if (!sink.isCancelled()) { |
||||
|
String completeMsg = CharacterStreamProcessor.formatMessage("complete", |
||||
|
"✅ " + getPerspectiveName(perspective) + "分析完成"); |
||||
|
sink.next(completeMsg); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"AI分析错误: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
if (!sink.isCancelled()) { |
||||
|
sink.next(CharacterStreamProcessor.formatMessage("error", |
||||
|
"系统错误: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 辅助方法:获取视角名称 |
||||
|
private String getPerspectiveName(String perspective) { |
||||
|
switch (perspective) { |
||||
|
case "management": return "管理"; |
||||
|
case "process": return "流程"; |
||||
|
case "culture": return "文化"; |
||||
|
case "comprehensive": return "综合"; |
||||
|
default: return "管理"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 辅助方法:加载视角提示词 |
||||
|
private String loadPerspectivePrompt(String perspective) throws IOException { |
||||
|
String promptFile = ""; |
||||
|
switch (perspective) { |
||||
|
case "management": promptFile = "prompts/management-perspective.txt"; break; |
||||
|
case "process": promptFile = "prompts/process-perspective.txt"; break; |
||||
|
case "culture": promptFile = "prompts/culture-perspective.txt"; break; |
||||
|
case "comprehensive": promptFile = "prompts/comprehensive-perspective.txt"; break; |
||||
|
default: promptFile = "prompts/management-perspective.txt"; break; |
||||
|
} |
||||
|
return promptLoader.loadPrompt(promptFile); |
||||
|
} |
||||
|
|
||||
|
// 辅助方法:构建完整的提示词 |
||||
|
private String buildPerspectivePrompt(String template, String data, |
||||
|
String deptName, String weekDisplay, String perspective) { |
||||
|
return template |
||||
|
.replace("{jsonData}", data) |
||||
|
.replace("{deptName}", deptName) |
||||
|
.replace("{weekDisplay}", weekDisplay) |
||||
|
.replace("{perspective}", getPerspectiveName(perspective)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,146 @@ |
|||||
|
package com.chenhai.chenhaiai.controller; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.Dept; |
||||
|
import com.chenhai.chenhaiai.entity.WeekProject; |
||||
|
import lombok.Data; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; |
||||
|
import org.springframework.ai.chat.model.ChatModel; |
||||
|
//import org.springframework.ai.ollama.api.OllamaChatOptions; |
||||
|
import org.springframework.ai.tool.ToolCallback; |
||||
|
import org.springframework.ai.tool.ToolCallbackProvider; |
||||
|
import org.springframework.core.ParameterizedTypeReference; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestParam; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import reactor.core.publisher.Flux; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-11-29 21:24 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/mcp") |
||||
|
public class McpController { |
||||
|
private final ChatClient chatClient; |
||||
|
private final ToolCallbackProvider toolCallbackProvider; |
||||
|
|
||||
|
public McpController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider toolCallbackProvider) { |
||||
|
this.toolCallbackProvider = toolCallbackProvider; |
||||
|
|
||||
|
// 调试:打印所有可用的工具 |
||||
|
List<ToolCallback> toolCallbacks = List.of(toolCallbackProvider.getToolCallbacks()); |
||||
|
System.out.println("=== 可用的 MCP 工具 ==="); |
||||
|
for (ToolCallback tool : toolCallbacks) { |
||||
|
System.out.println("工具: " + tool.toString()); |
||||
|
} |
||||
|
System.out.println("===================="); |
||||
|
|
||||
|
this.chatClient = chatClientBuilder |
||||
|
.defaultToolCallbacks(toolCallbacks) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
// public McpController(ToolCallbackProvider toolCallbackProvider, ChatModel chatModel) { |
||||
|
// this.toolCallbackProvider = toolCallbackProvider; |
||||
|
// |
||||
|
// // 调试:打印所有可用的工具 |
||||
|
// List<ToolCallback> toolCallbacks = List.of(toolCallbackProvider.getToolCallbacks()); |
||||
|
// System.out.println("=== 可用的 MCP 工具 ==="); |
||||
|
// for (ToolCallback tool : toolCallbacks) { |
||||
|
// System.out.println("工具: " + tool.toString()); |
||||
|
// } |
||||
|
// System.out.println("===================="); |
||||
|
// |
||||
|
// this.chatClient = ChatClient.builder(chatModel) |
||||
|
// // 实现 Logger 的 Advisor |
||||
|
// .defaultAdvisors( |
||||
|
// new SimpleLoggerAdvisor() |
||||
|
// ) |
||||
|
// // 设置 ChatClient 中 ChatModel 的 Options 参数 |
||||
|
// .defaultOptions( |
||||
|
// OllamaChatOptions.builder() |
||||
|
// .topP(0.7) |
||||
|
// .model("deepseek-v2:latest") |
||||
|
// .build() |
||||
|
// ) |
||||
|
// .defaultToolCallbacks(toolCallbacks) |
||||
|
// .build(); |
||||
|
// } |
||||
|
|
||||
|
@GetMapping("/simple-test") |
||||
|
public String simpleTest() { |
||||
|
try { |
||||
|
// 最简单的调用 |
||||
|
String response = chatClient.prompt() |
||||
|
.user("你好") |
||||
|
.call() |
||||
|
.content(); |
||||
|
return "Success: " + response; |
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
return "Error: " + e.getMessage(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/chats") |
||||
|
public Flux<String> chats(@RequestParam String question) { |
||||
|
return chatClient.prompt(question).stream().content(); |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/chat") |
||||
|
public Flux<String> chat(@RequestParam String question) { |
||||
|
String systemPrompt = """ |
||||
|
你是一个mysql查询专家,可以调用工具执行指定sql语句查询, |
||||
|
请严格按照MySQL查询结果的原始结构整理数据,保持每行记录的独立性: |
||||
|
|
||||
|
**要求:** |
||||
|
1. 保持原始的行级数据,不要合并任何行 |
||||
|
2. 每行都是一个独立的任务记录 |
||||
|
3. 表头用中文:项目名称 | 任务内容 | 负责人 |
||||
|
4. 所有列左对齐 |
||||
|
5. 在每一行数据结尾处位置添加`<br>`标签确保在浏览器中自动换行 |
||||
|
6. 不要改变任何原始的人员对应关系 |
||||
|
|
||||
|
**正确的格式示例:** |
||||
|
| 项目名称 | 任务内容 | 负责人 | |
||||
|
|:---------|:---------|:-------| |
||||
|
| 项目A | 具体任务描述1 | 张三 | |
||||
|
| 项目A | 具体任务描述2 | 李四 | |
||||
|
| 项目B | 具体任务描述3 | 王五 | |
||||
|
|
||||
|
请直接输出整理后的表格。 |
||||
|
"""; |
||||
|
Flux<String> stringFlux = chatClient.prompt() |
||||
|
.system(systemPrompt) |
||||
|
.user(question) |
||||
|
.stream() |
||||
|
.content(); |
||||
|
return stringFlux; |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/entity") |
||||
|
public List<WeekProject> response() { |
||||
|
List<WeekProject> list = chatClient.prompt() |
||||
|
.user("请使用可用的工具查询ch_week_project表main_id=7的所有数据,只保留字段project_name,content,developer") |
||||
|
.call().entity(new ParameterizedTypeReference<List<WeekProject>>() {}); |
||||
|
return list; |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/chat2") |
||||
|
public Dept chat2(@RequestParam String deptName) { |
||||
|
String prompt = """ |
||||
|
请使用可用的工具查询sys_dept表dept_name=%s的唯一数据,只保留字段dept_id,dept_name |
||||
|
""".formatted(deptName); |
||||
|
|
||||
|
Dept dept = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.entity(Dept.class); |
||||
|
return dept; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
package com.chenhai.chenhaiai.controller; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.config.ChatModelFactory; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* AI模型管理控制器 |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/ai/model") |
||||
|
public class ModelController { |
||||
|
|
||||
|
private final ChatModelFactory modelFactory; |
||||
|
|
||||
|
// 构造器注入 |
||||
|
public ModelController(ChatModelFactory modelFactory) { |
||||
|
this.modelFactory = modelFactory; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取可用模型列表 |
||||
|
*/ |
||||
|
@GetMapping("/list") |
||||
|
public AjaxResult listModels() { |
||||
|
return AjaxResult.success(modelFactory.getAvailableModels()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 测试指定模型 |
||||
|
*/ |
||||
|
@PostMapping("/test") |
||||
|
public AjaxResult testModel(@RequestParam String model) { |
||||
|
try { |
||||
|
ChatClient client = modelFactory.getChatClient(model); |
||||
|
String response = client.prompt() |
||||
|
.user("你好,请回复'连接测试成功'") |
||||
|
.call() |
||||
|
.content(); |
||||
|
|
||||
|
return AjaxResult.success("测试成功", Map.of( |
||||
|
"model", model, |
||||
|
"response", response, |
||||
|
"timestamp", System.currentTimeMillis() |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
return AjaxResult.error("测试失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取默认模型 |
||||
|
*/ |
||||
|
@GetMapping("/default") |
||||
|
public AjaxResult getDefaultModel() { |
||||
|
return AjaxResult.success("默认模型", modelFactory.getDefaultModel()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取模型详情 |
||||
|
*/ |
||||
|
@GetMapping("/info") |
||||
|
public AjaxResult getModelInfo(@RequestParam String model) { |
||||
|
try { |
||||
|
ChatClient client = modelFactory.getChatClient(model); |
||||
|
String provider = modelFactory.getModelProvider(model); |
||||
|
|
||||
|
return AjaxResult.success("模型信息", Map.of( |
||||
|
"model", model, |
||||
|
"provider", provider, |
||||
|
"available", true, |
||||
|
"description", getModelDescription(model) |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
return AjaxResult.error("模型不可用"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量测试所有模型 |
||||
|
*/ |
||||
|
@PostMapping("/test-all") |
||||
|
public AjaxResult testAllModels() { |
||||
|
try { |
||||
|
Map<String, Object> results = new java.util.HashMap<>(); |
||||
|
var models = modelFactory.getAvailableModels(); |
||||
|
|
||||
|
for (var modelInfo : models) { |
||||
|
String model = modelInfo.getValue(); |
||||
|
try { |
||||
|
ChatClient client = modelFactory.getChatClient(model); |
||||
|
String response = client.prompt() |
||||
|
.user("测试,就问问你是谁,你是多少参数的版本") |
||||
|
.call() |
||||
|
.content(); |
||||
|
|
||||
|
results.put(model, Map.of( |
||||
|
"success", true, |
||||
|
"response", response.substring(0, Math.min(50, response.length())) + "..." |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
results.put(model, Map.of( |
||||
|
"success", false, |
||||
|
"error", e.getMessage() |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return AjaxResult.success("批量测试完成", results); |
||||
|
} catch (Exception e) { |
||||
|
return AjaxResult.error("批量测试失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String getModelDescription(String model) { |
||||
|
if (model.startsWith("glm")) { |
||||
|
return "智谱AI大模型"; |
||||
|
} else { |
||||
|
return "本地Ollama模型"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
package com.chenhai.chenhaiai.controller; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.service.gitNew.GiteaDataService; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
|
||||
|
@RestController |
||||
|
@RequestMapping("/test/real") |
||||
|
public class RealTestController { |
||||
|
|
||||
|
@Autowired |
||||
|
private GiteaDataService giteaDataService; |
||||
|
|
||||
|
@GetMapping("/sync") |
||||
|
public String runRealSyncTest() { |
||||
|
new Thread(() -> { |
||||
|
giteaDataService.realSyncTest(); |
||||
|
}).start(); |
||||
|
|
||||
|
return "真实同步测试已开始,请查看控制台日志!"; |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/full-quick") |
||||
|
public String testFullQuick() { |
||||
|
new Thread(() -> { |
||||
|
giteaDataService.fullSyncTest(); |
||||
|
}).start(); |
||||
|
return "快速全量测试已开始(前5个仓库),请查看控制台日志!"; |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/full-all") |
||||
|
public String testFullAll() { |
||||
|
new Thread(() -> { |
||||
|
giteaDataService.executeFullSync(); |
||||
|
}).start(); |
||||
|
return "全量同步已开始(所有183个仓库),请查看控制台日志!"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.DeveloperActivity; |
||||
|
import com.chenhai.chenhaiai.entity.git.RepositoryActivity; |
||||
|
import lombok.Data; |
||||
|
import java.time.DayOfWeek; |
||||
|
import java.time.LocalDateTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.*; |
||||
|
|
||||
|
@Data |
||||
|
public class AnalysisResult { |
||||
|
private String analysisId; |
||||
|
private LocalDateTime analysisTime; |
||||
|
private String timeRange; |
||||
|
private int totalRepositories; |
||||
|
private int activeRepositories; |
||||
|
private int totalCommits; |
||||
|
private int totalDevelopers; |
||||
|
|
||||
|
private Map<String, DeveloperActivity> developerActivities = new HashMap<>(); |
||||
|
private Map<String, RepositoryActivity> repositoryActivities = new HashMap<>(); |
||||
|
|
||||
|
// 汇总统计 |
||||
|
private Map<DayOfWeek, Integer> overallCommitsByDay = new EnumMap<>(DayOfWeek.class); |
||||
|
private Map<Integer, Integer> overallCommitsByHour = new HashMap<>(); |
||||
|
private Map<String, Integer> overallFileTypeDistribution = new HashMap<>(); |
||||
|
|
||||
|
// 排行榜 - 修改类型声明 |
||||
|
private List<Map.Entry<String, DeveloperActivity>> developerRanking = new ArrayList<>(); |
||||
|
private List<Map.Entry<String, RepositoryActivity>> repositoryRanking = new ArrayList<>(); |
||||
|
|
||||
|
public AnalysisResult() { |
||||
|
this.analysisId = UUID.randomUUID().toString(); |
||||
|
this.analysisTime = LocalDateTime.now(); |
||||
|
} |
||||
|
|
||||
|
public void calculateSummary() { |
||||
|
// 汇总开发者数据 |
||||
|
for (DeveloperActivity activity : developerActivities.values()) { |
||||
|
// 汇总时间分布 |
||||
|
activity.getCommitsByDay().forEach((day, count) -> |
||||
|
overallCommitsByDay.put(day, overallCommitsByDay.getOrDefault(day, 0) + count)); |
||||
|
|
||||
|
activity.getCommitsByHour().forEach((hour, count) -> |
||||
|
overallCommitsByHour.put(hour, overallCommitsByHour.getOrDefault(hour, 0) + count)); |
||||
|
|
||||
|
// 汇总文件类型 |
||||
|
activity.getCommitsByFileType().forEach((fileType, count) -> |
||||
|
overallFileTypeDistribution.put(fileType, |
||||
|
overallFileTypeDistribution.getOrDefault(fileType, 0) + count)); |
||||
|
} |
||||
|
|
||||
|
// 生成排行榜 - 直接使用entrySet() |
||||
|
developerRanking = new ArrayList<>(developerActivities.entrySet()); |
||||
|
developerRanking.sort((a, b) -> Integer.compare(b.getValue().getTotalCommits(), |
||||
|
a.getValue().getTotalCommits())); |
||||
|
|
||||
|
repositoryRanking = new ArrayList<>(repositoryActivities.entrySet()); |
||||
|
repositoryRanking.sort((a, b) -> Integer.compare(b.getValue().getTotalCommits(), |
||||
|
a.getValue().getTotalCommits())); |
||||
|
|
||||
|
// 计算总数 |
||||
|
totalDevelopers = developerActivities.size(); |
||||
|
totalRepositories = repositoryActivities.size(); |
||||
|
totalCommits = developerActivities.values().stream() |
||||
|
.mapToInt(DeveloperActivity::getTotalCommits) |
||||
|
.sum(); |
||||
|
} |
||||
|
|
||||
|
public String getFormattedAnalysisTime() { |
||||
|
return analysisTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); |
||||
|
} |
||||
|
|
||||
|
public String generateSummary() { |
||||
|
// 获取排行榜数据 |
||||
|
String topDeveloper = "无"; |
||||
|
int topDeveloperCommits = 0; |
||||
|
String topRepository = "无"; |
||||
|
int topRepositoryCommits = 0; |
||||
|
|
||||
|
if (!developerRanking.isEmpty()) { |
||||
|
topDeveloper = developerRanking.get(0).getKey(); |
||||
|
topDeveloperCommits = developerRanking.get(0).getValue().getTotalCommits(); |
||||
|
} |
||||
|
|
||||
|
if (!repositoryRanking.isEmpty()) { |
||||
|
topRepository = repositoryRanking.get(0).getKey(); |
||||
|
topRepositoryCommits = repositoryRanking.get(0).getValue().getTotalCommits(); |
||||
|
} |
||||
|
|
||||
|
return String.format( |
||||
|
"分析时间: %s\n" + |
||||
|
"时间范围: %s\n" + |
||||
|
"仓库总数: %d (活跃: %d)\n" + |
||||
|
"开发者总数: %d\n" + |
||||
|
"总提交数: %d\n" + |
||||
|
"最活跃开发者: %s (%d 次提交)\n" + |
||||
|
"最活跃仓库: %s (%d 次提交)", |
||||
|
getFormattedAnalysisTime(), |
||||
|
timeRange, |
||||
|
totalRepositories, activeRepositories, |
||||
|
totalDevelopers, |
||||
|
totalCommits, |
||||
|
topDeveloper, topDeveloperCommits, |
||||
|
topRepository, topRepositoryCommits |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.time.LocalDate; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:42 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class DailyPaper { |
||||
|
private String projectName; |
||||
|
private String content; |
||||
|
private String dailyPaperDate; |
||||
|
private String dailyPaperHour; |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 20:20 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class Dept { |
||||
|
/** |
||||
|
* 部门ID |
||||
|
*/ |
||||
|
private Long deptId; |
||||
|
/** |
||||
|
* 部门名称 |
||||
|
*/ |
||||
|
private String deptName; |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:26 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class UserInfo { |
||||
|
private Long userId; |
||||
|
private String userName; |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:41 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class WeekPlanDetail { |
||||
|
private Long mainId; |
||||
|
private String projectName; |
||||
|
private String content; |
||||
|
private String developer; |
||||
|
private Integer superviseStatus; |
||||
|
private String note; |
||||
|
private String projectNote; |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:40 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class WeekPlanMain { |
||||
|
private Long id; |
||||
|
private String deptName; |
||||
|
private String weekDisplay; |
||||
|
private String weekStartDate; |
||||
|
private String weekEndDate; |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.GitAnalysisData; |
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import lombok.Data; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 18:49 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class WeekPlanResponse { |
||||
|
|
||||
|
/** |
||||
|
* 部门信息 |
||||
|
*/ |
||||
|
private String deptName; |
||||
|
|
||||
|
/** |
||||
|
* 周次显示 |
||||
|
*/ |
||||
|
private String weekDisplay; |
||||
|
|
||||
|
/** |
||||
|
* 部门信息 |
||||
|
*/ |
||||
|
private Dept dept; |
||||
|
|
||||
|
/** |
||||
|
* 员工列表 |
||||
|
*/ |
||||
|
private List<UserInfo> userInfos; |
||||
|
|
||||
|
/** |
||||
|
* 周计划主信息 |
||||
|
*/ |
||||
|
private WeekPlanMain planMain; |
||||
|
|
||||
|
/** |
||||
|
* 周计划详情列表 |
||||
|
*/ |
||||
|
private List<WeekPlanDetail> planDetails; |
||||
|
|
||||
|
/** |
||||
|
* 日报列表 |
||||
|
*/ |
||||
|
private List<DailyPaper> dailyPapers; |
||||
|
|
||||
|
/** |
||||
|
* Git分析结果 |
||||
|
*/ |
||||
|
private GitAnalysisData gitAnalysis; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
package com.chenhai.chenhaiai.entity; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-11-29 22:47 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class WeekProject { |
||||
|
|
||||
|
private String projectName; |
||||
|
private String content; |
||||
|
private String developer; |
||||
|
|
||||
|
public String getProjectName() { |
||||
|
return projectName; |
||||
|
} |
||||
|
|
||||
|
public void setProjectName(String projectName) { |
||||
|
this.projectName = projectName; |
||||
|
} |
||||
|
|
||||
|
public String getContent() { |
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
public void setContent(String content) { |
||||
|
this.content = content; |
||||
|
} |
||||
|
|
||||
|
public String getDeveloper() { |
||||
|
return developer; |
||||
|
} |
||||
|
|
||||
|
public void setDeveloper(String developer) { |
||||
|
this.developer = developer; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import com.fasterxml.jackson.annotation.JsonInclude; |
||||
|
import lombok.Data; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) // 关键注解 |
||||
|
public class BasicInfo { |
||||
|
private String timeRange; |
||||
|
private int totalRepos; |
||||
|
private int activeRepos; |
||||
|
private int activeDevelopers; |
||||
|
private int totalCommits; |
||||
|
private long analysisTime; |
||||
|
private String analysisStrategy; |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class DayStats { |
||||
|
private String dayName; |
||||
|
private int commitCount; |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.GiteaCommit; |
||||
|
import lombok.Data; |
||||
|
import java.time.DayOfWeek; |
||||
|
import java.time.ZonedDateTime; |
||||
|
import java.util.*; |
||||
|
|
||||
|
@Data |
||||
|
public class DeveloperActivity { |
||||
|
private String developerName; |
||||
|
private String developerEmail; |
||||
|
private int totalCommits; |
||||
|
private Set<String> contributedRepos = new HashSet<>(); |
||||
|
private Map<DayOfWeek, Integer> commitsByDay = new EnumMap<>(DayOfWeek.class); |
||||
|
private Map<Integer, Integer> commitsByHour = new HashMap<>(); |
||||
|
private Map<String, Integer> commitsByFileType = new HashMap<>(); |
||||
|
private List<String> recentCommitMessages = new ArrayList<>(); |
||||
|
// 新增行数字段 |
||||
|
private int totalAdditions = 0; |
||||
|
private int totalDeletions = 0; |
||||
|
|
||||
|
// 新增方法:累加行数变化 |
||||
|
public void addLineChanges(int additions, int deletions) { |
||||
|
this.totalAdditions += additions; |
||||
|
this.totalDeletions += deletions; |
||||
|
} |
||||
|
|
||||
|
// 获取净增行数 |
||||
|
public int getNetLineChanges() { |
||||
|
return totalAdditions - totalDeletions; |
||||
|
} |
||||
|
|
||||
|
// 计算属性 |
||||
|
public double getAvgCommitsPerDay() { |
||||
|
return totalCommits > 0 ? totalCommits / 7.0 : 0; |
||||
|
} |
||||
|
|
||||
|
public String getMostActiveDay() { |
||||
|
return commitsByDay.entrySet().stream() |
||||
|
.max(Map.Entry.comparingByValue()) |
||||
|
.map(entry -> entry.getKey().toString()) |
||||
|
.orElse("Unknown"); |
||||
|
} |
||||
|
|
||||
|
public String getMostActiveHour() { |
||||
|
return commitsByHour.entrySet().stream() |
||||
|
.max(Map.Entry.comparingByValue()) |
||||
|
.map(entry -> String.format("%02d:00", entry.getKey())) |
||||
|
.orElse("Unknown"); |
||||
|
} |
||||
|
|
||||
|
public String getMostActiveFileType() { |
||||
|
return commitsByFileType.entrySet().stream() |
||||
|
.max(Map.Entry.comparingByValue()) |
||||
|
.map(Map.Entry::getKey) |
||||
|
.orElse("Unknown"); |
||||
|
} |
||||
|
|
||||
|
public void addCommit(GiteaCommit commit, String repoFullName) { |
||||
|
totalCommits++; |
||||
|
contributedRepos.add(repoFullName); |
||||
|
|
||||
|
// 时间分析 |
||||
|
ZonedDateTime commitTime = commit.getCommitTime(); |
||||
|
if (commitTime != null) { |
||||
|
DayOfWeek dayOfWeek = commitTime.getDayOfWeek(); |
||||
|
commitsByDay.put(dayOfWeek, commitsByDay.getOrDefault(dayOfWeek, 0) + 1); |
||||
|
|
||||
|
int hour = commitTime.getHour(); |
||||
|
commitsByHour.put(hour, commitsByHour.getOrDefault(hour, 0) + 1); |
||||
|
} |
||||
|
|
||||
|
// 文件类型分析 |
||||
|
if (commit.getFiles() != null) { |
||||
|
for (GiteaCommit.ChangedFile file : commit.getFiles()) { |
||||
|
String fileType = file.getFileType(); |
||||
|
commitsByFileType.put(fileType, |
||||
|
commitsByFileType.getOrDefault(fileType, 0) + 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 保存最近提交消息 |
||||
|
if (commit.getCommit() != null && commit.getCommit().getMessage() != null) { |
||||
|
String message = commit.getCommit().getMessage().trim(); |
||||
|
if (!message.isEmpty()) { |
||||
|
recentCommitMessages.add(message); |
||||
|
if (recentCommitMessages.size() > 10) { |
||||
|
recentCommitMessages.remove(0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class DeveloperRank { |
||||
|
private int rank; |
||||
|
private String name; |
||||
|
private int commitCount; |
||||
|
private int repoCount; |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class FileTypeStats { |
||||
|
private String fileType; |
||||
|
private int fileCount; |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import com.fasterxml.jackson.annotation.JsonInclude; |
||||
|
import lombok.Data; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) // 关键注解 |
||||
|
public class GitAnalysisData { |
||||
|
private BasicInfo basicInfo; |
||||
|
private List<DeveloperRank> developerRanks; |
||||
|
private List<RepoRank> repoRanks; |
||||
|
private List<DayStats> dayStats; |
||||
|
private List<FileTypeStats> fileTypeStats; |
||||
|
private String generatedTime; |
||||
|
private String rawReport; |
||||
|
} |
||||
@ -0,0 +1,203 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
import lombok.Data; |
||||
|
import java.time.ZonedDateTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) // 添加这个注解 |
||||
|
public class GiteaCommit { |
||||
|
@JsonProperty("sha") |
||||
|
private String sha; |
||||
|
|
||||
|
@JsonProperty("commit") |
||||
|
private CommitInfo commit; |
||||
|
|
||||
|
@JsonProperty("html_url") |
||||
|
private String htmlUrl; |
||||
|
|
||||
|
@JsonProperty("author") |
||||
|
private UserInfo author; |
||||
|
|
||||
|
@JsonProperty("committer") |
||||
|
private UserInfo committer; |
||||
|
|
||||
|
@JsonProperty("parents") |
||||
|
private List<ParentCommit> parents; |
||||
|
|
||||
|
@JsonProperty("files") |
||||
|
private List<ChangedFile> files; |
||||
|
|
||||
|
// Gitea API 可能返回的其他字段 |
||||
|
@JsonProperty("stats") |
||||
|
private Stats stats; |
||||
|
|
||||
|
@JsonProperty("url") |
||||
|
private String url; |
||||
|
|
||||
|
// 实用方法 |
||||
|
public String getShortSha() { |
||||
|
return sha != null && sha.length() > 7 ? sha.substring(0, 7) : sha; |
||||
|
} |
||||
|
|
||||
|
public String getShortMessage() { |
||||
|
if (commit == null || commit.getMessage() == null) return ""; |
||||
|
String message = commit.getMessage().trim(); |
||||
|
return message.length() > 50 ? message.substring(0, 47) + "..." : message; |
||||
|
} |
||||
|
|
||||
|
public ZonedDateTime getCommitTime() { |
||||
|
if (commit != null && commit.getAuthor() != null && commit.getAuthor().getDate() != null) { |
||||
|
return ZonedDateTime.parse(commit.getAuthor().getDate()); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public boolean isWithinTimeRange(ZonedDateTime since, ZonedDateTime until) { |
||||
|
ZonedDateTime commitTime = getCommitTime(); |
||||
|
return commitTime != null && |
||||
|
!commitTime.isBefore(since) && |
||||
|
!commitTime.isAfter(until); |
||||
|
} |
||||
|
|
||||
|
// 内部类 |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public static class CommitInfo { |
||||
|
@JsonProperty("message") |
||||
|
private String message; |
||||
|
|
||||
|
@JsonProperty("author") |
||||
|
private CommitAuthor author; |
||||
|
|
||||
|
@JsonProperty("committer") |
||||
|
private CommitAuthor committer; |
||||
|
|
||||
|
@JsonProperty("url") |
||||
|
private String url; |
||||
|
|
||||
|
@JsonProperty("comment_count") |
||||
|
private Integer commentCount; |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public static class CommitAuthor { |
||||
|
@JsonProperty("name") |
||||
|
private String name; |
||||
|
|
||||
|
@JsonProperty("email") |
||||
|
private String email; |
||||
|
|
||||
|
@JsonProperty("date") |
||||
|
private String date; |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public static class UserInfo { |
||||
|
@JsonProperty("id") |
||||
|
private Long id; |
||||
|
|
||||
|
@JsonProperty("login") |
||||
|
private String login; |
||||
|
|
||||
|
@JsonProperty("full_name") |
||||
|
private String fullName; |
||||
|
|
||||
|
@JsonProperty("email") |
||||
|
private String email; |
||||
|
|
||||
|
@JsonProperty("avatar_url") |
||||
|
private String avatarUrl; |
||||
|
|
||||
|
@JsonProperty("language") |
||||
|
private String language; |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public static class ParentCommit { |
||||
|
@JsonProperty("sha") |
||||
|
private String sha; |
||||
|
|
||||
|
@JsonProperty("url") |
||||
|
private String url; |
||||
|
|
||||
|
@JsonProperty("html_url") |
||||
|
private String htmlUrl; |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public static class ChangedFile { |
||||
|
@JsonProperty("filename") |
||||
|
private String filename; |
||||
|
|
||||
|
@JsonProperty("status") |
||||
|
private String status; // added, modified, removed, renamed |
||||
|
|
||||
|
@JsonProperty("additions") |
||||
|
private Integer additions; |
||||
|
|
||||
|
@JsonProperty("deletions") |
||||
|
private Integer deletions; |
||||
|
|
||||
|
@JsonProperty("changes") |
||||
|
private Integer changes; |
||||
|
|
||||
|
@JsonProperty("patch") |
||||
|
private String patch; |
||||
|
|
||||
|
@JsonProperty("raw_url") |
||||
|
private String rawUrl; |
||||
|
|
||||
|
// 注意:Gitea API 可能不返回 additions/deletions,但我们仍然定义字段 |
||||
|
|
||||
|
public String getFileExtension() { |
||||
|
if (filename == null) return ""; |
||||
|
int dotIndex = filename.lastIndexOf('.'); |
||||
|
return dotIndex > 0 ? filename.substring(dotIndex + 1).toLowerCase() : ""; |
||||
|
} |
||||
|
|
||||
|
public String getFileType() { |
||||
|
String ext = getFileExtension(); |
||||
|
if (ext.isEmpty()) return "unknown"; |
||||
|
|
||||
|
if (ext.matches("(java|py|js|ts|cpp|c|go|rs|php|rb|scala|kt|swift)")) { |
||||
|
return "code"; |
||||
|
} else if (ext.matches("(json|yml|yaml|xml|properties|conf|ini|toml)")) { |
||||
|
return "config"; |
||||
|
} else if (ext.matches("(md|txt|rst|adoc|docx|pdf)")) { |
||||
|
return "document"; |
||||
|
} else if (ext.matches("(sql|ddl)")) { |
||||
|
return "database"; |
||||
|
} else if (ext.matches("(html|css|scss|less)")) { |
||||
|
return "frontend"; |
||||
|
} else if (ext.matches("(jpg|jpeg|png|gif|svg|ico)")) { |
||||
|
return "image"; |
||||
|
} else { |
||||
|
return "other"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public static class Stats { |
||||
|
@JsonProperty("total") |
||||
|
private Integer total; |
||||
|
|
||||
|
@JsonProperty("additions") |
||||
|
private Integer additions; |
||||
|
|
||||
|
@JsonProperty("deletions") |
||||
|
private Integer deletions; |
||||
|
|
||||
|
@JsonProperty("files_changed") |
||||
|
private Integer filesChanged; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) // 添加这个注解 |
||||
|
public class GiteaRepository { |
||||
|
@JsonProperty("id") |
||||
|
private Long id; |
||||
|
|
||||
|
@JsonProperty("name") |
||||
|
private String name; |
||||
|
|
||||
|
@JsonProperty("full_name") |
||||
|
private String fullName; |
||||
|
|
||||
|
@JsonProperty("owner") |
||||
|
private GiteaUser owner; |
||||
|
|
||||
|
@JsonProperty("description") |
||||
|
private String description; |
||||
|
|
||||
|
@JsonProperty("html_url") |
||||
|
private String htmlUrl; |
||||
|
|
||||
|
@JsonProperty("ssh_url") |
||||
|
private String sshUrl; |
||||
|
|
||||
|
@JsonProperty("clone_url") |
||||
|
private String cloneUrl; |
||||
|
|
||||
|
@JsonProperty("default_branch") |
||||
|
private String defaultBranch; |
||||
|
|
||||
|
@JsonProperty("created_at") |
||||
|
private String createdAt; |
||||
|
|
||||
|
@JsonProperty("updated_at") |
||||
|
private String updatedAt; |
||||
|
|
||||
|
@JsonProperty("size") |
||||
|
private Integer size; |
||||
|
|
||||
|
@JsonProperty("stars_count") |
||||
|
private Integer starsCount; |
||||
|
|
||||
|
@JsonProperty("forks_count") |
||||
|
private Integer forksCount; |
||||
|
|
||||
|
@JsonProperty("open_issues_count") |
||||
|
private Integer openIssuesCount; |
||||
|
|
||||
|
@JsonProperty("private") |
||||
|
private Boolean isPrivate; |
||||
|
|
||||
|
// Gitea API 可能返回的其他字段 |
||||
|
@JsonProperty("language") |
||||
|
private String language; |
||||
|
|
||||
|
@JsonProperty("has_issues") |
||||
|
private Boolean hasIssues; |
||||
|
|
||||
|
@JsonProperty("has_wiki") |
||||
|
private Boolean hasWiki; |
||||
|
|
||||
|
@JsonProperty("has_projects") |
||||
|
private Boolean hasProjects; |
||||
|
|
||||
|
@JsonProperty("archived") |
||||
|
private Boolean archived; |
||||
|
|
||||
|
// 兼容性方法 |
||||
|
public String getFullPath() { |
||||
|
return fullName != null ? fullName : ""; |
||||
|
} |
||||
|
|
||||
|
public String getRepoName() { |
||||
|
return name != null ? name : |
||||
|
(fullName != null && fullName.contains("/") ? |
||||
|
fullName.substring(fullName.lastIndexOf("/") + 1) : ""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) // 添加这个注解,忽略未知字段 |
||||
|
class GiteaUser { |
||||
|
@JsonProperty("id") |
||||
|
private Long id; |
||||
|
|
||||
|
@JsonProperty("login") |
||||
|
private String login; |
||||
|
|
||||
|
@JsonProperty("full_name") |
||||
|
private String fullName; |
||||
|
|
||||
|
@JsonProperty("email") |
||||
|
private String email; |
||||
|
|
||||
|
@JsonProperty("avatar_url") |
||||
|
private String avatarUrl; |
||||
|
|
||||
|
// Gitea API 可能返回的其他字段 |
||||
|
@JsonProperty("language") |
||||
|
private String language; |
||||
|
|
||||
|
@JsonProperty("is_admin") |
||||
|
private Boolean isAdmin; |
||||
|
|
||||
|
@JsonProperty("last_login") |
||||
|
private String lastLogin; |
||||
|
|
||||
|
@JsonProperty("created") |
||||
|
private String created; |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class RepoRank { |
||||
|
private int rank; |
||||
|
private String repoName; |
||||
|
private String displayName; |
||||
|
private int commitCount; |
||||
|
private int developerCount; |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
package com.chenhai.chenhaiai.entity.git; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.GiteaCommit; |
||||
|
import lombok.Data; |
||||
|
import java.time.LocalDate; |
||||
|
import java.util.*; |
||||
|
|
||||
|
@Data |
||||
|
public class RepositoryActivity { |
||||
|
private String repoFullName; |
||||
|
private String repoName; |
||||
|
private int totalCommits; |
||||
|
private Set<String> activeDevelopers = new HashSet<>(); |
||||
|
private Map<LocalDate, Integer> commitsByDate = new HashMap<>(); |
||||
|
private Map<String, Integer> fileTypeDistribution = new HashMap<>(); |
||||
|
private Map<String, Integer> developerCommitCount = new HashMap<>(); |
||||
|
private List<String> recentCommitShas = new ArrayList<>(); |
||||
|
// 新增行数字段 |
||||
|
private int totalAdditions = 0; |
||||
|
private int totalDeletions = 0; |
||||
|
|
||||
|
// 新增方法:添加提交并记录行数 |
||||
|
// public void addCommitWithLines(GiteaCommit commit, GiteaAnalysisNewService.CommitLineChange lineChange) { |
||||
|
// // 调用原有addCommit方法维护其他统计 |
||||
|
// addCommit(commit); |
||||
|
// |
||||
|
// // 累加行数 |
||||
|
// this.totalAdditions += lineChange.getAdditions(); |
||||
|
// this.totalDeletions += lineChange.getDeletions(); |
||||
|
// } |
||||
|
|
||||
|
// 获取净增行数 |
||||
|
public int getNetLineChanges() { |
||||
|
return totalAdditions - totalDeletions; |
||||
|
} |
||||
|
|
||||
|
// 计算属性 |
||||
|
public int getDeveloperCount() { |
||||
|
return activeDevelopers.size(); |
||||
|
} |
||||
|
|
||||
|
public double getAvgCommitsPerDeveloper() { |
||||
|
return activeDevelopers.isEmpty() ? 0 : |
||||
|
(double) totalCommits / activeDevelopers.size(); |
||||
|
} |
||||
|
|
||||
|
public String getMostActiveDeveloper() { |
||||
|
return developerCommitCount.entrySet().stream() |
||||
|
.max(Map.Entry.comparingByValue()) |
||||
|
.map(Map.Entry::getKey) |
||||
|
.orElse("Unknown"); |
||||
|
} |
||||
|
|
||||
|
public String getMostChangedFileType() { |
||||
|
return fileTypeDistribution.entrySet().stream() |
||||
|
.max(Map.Entry.comparingByValue()) |
||||
|
.map(Map.Entry::getKey) |
||||
|
.orElse("Unknown"); |
||||
|
} |
||||
|
|
||||
|
public void addCommit(GiteaCommit commit) { |
||||
|
totalCommits++; |
||||
|
|
||||
|
// 开发者信息 |
||||
|
if (commit.getCommit() != null && commit.getCommit().getAuthor() != null) { |
||||
|
String developer = commit.getCommit().getAuthor().getName() != null ? |
||||
|
commit.getCommit().getAuthor().getName() : |
||||
|
commit.getCommit().getAuthor().getEmail(); |
||||
|
|
||||
|
activeDevelopers.add(developer); |
||||
|
developerCommitCount.put(developer, |
||||
|
developerCommitCount.getOrDefault(developer, 0) + 1); |
||||
|
} |
||||
|
|
||||
|
// 日期统计 |
||||
|
if (commit.getCommitTime() != null) { |
||||
|
LocalDate date = commit.getCommitTime().toLocalDate(); |
||||
|
commitsByDate.put(date, commitsByDate.getOrDefault(date, 0) + 1); |
||||
|
} |
||||
|
|
||||
|
// 文件类型统计 |
||||
|
if (commit.getFiles() != null) { |
||||
|
for (GiteaCommit.ChangedFile file : commit.getFiles()) { |
||||
|
String fileType = file.getFileType(); |
||||
|
fileTypeDistribution.put(fileType, |
||||
|
fileTypeDistribution.getOrDefault(fileType, 0) + 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 保存最近提交 |
||||
|
if (commit.getSha() != null) { |
||||
|
recentCommitShas.add(commit.getShortSha()); |
||||
|
if (recentCommitShas.size() > 5) { |
||||
|
recentCommitShas.remove(0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,135 @@ |
|||||
|
package com.chenhai.chenhaiai.node; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-11-28 16:09 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class DataAssociationNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public DataAssociationNode(ChatClient.Builder builder) { |
||||
|
this.chatClient = builder.build(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 输入数据 |
||||
|
String dataOrganizationResult = state.value("dataOrganizationResult", ""); |
||||
|
|
||||
|
// 使用文本块语法,避免在模板中使用 {示例字段} 这种会被误解析的语法 |
||||
|
String promptTemplate = """ |
||||
|
你是一个数据关联分析助手,需要将日报数据与周计划任务进行智能关联,并输出完整的关联后数据结构。 |
||||
|
|
||||
|
### 任务要求: |
||||
|
1. 分析日报工作内容与周计划任务的相似性,进行智能匹配 |
||||
|
2. 将关联的日报数据添加到对应周计划任务的"关联日报"字段中 |
||||
|
3. 无法关联的日报数据,在项目列表中创建"临时任务"项目进行统一归类 |
||||
|
4. 保持原有的数据结构不变,只增加"关联日报"字段和"临时任务"项目 |
||||
|
|
||||
|
### 关联匹配规则: |
||||
|
1. **关键词匹配**:日报工作内容与周计划任务内容的关键词相似度 |
||||
|
- 完全匹配:工作内容完全一致或高度相似 |
||||
|
- 部分匹配:包含相同关键词或业务领域相同 |
||||
|
|
||||
|
2. **负责人匹配**:日报负责人与周计划任务负责人的重叠度 |
||||
|
- 负责人完全一致优先匹配 |
||||
|
- 负责人有重叠部分次优先匹配 |
||||
|
|
||||
|
3. **项目一致性**:日报项目名称与周计划项目名称的对应关系 |
||||
|
- "研发部工作"日报可匹配到所有研发相关周计划项目 |
||||
|
|
||||
|
4. **语义关联**:理解工作内容的业务含义进行关联 |
||||
|
|
||||
|
### 输出数据结构要求: |
||||
|
|
||||
|
必须严格按照以下完整结构输出: |
||||
|
|
||||
|
周计划数据: |
||||
|
部门名称: "[实际部门名称]" |
||||
|
周次显示: "[实际周次显示]" |
||||
|
项目列表: |
||||
|
- 项目名称: "[实际项目名称1]" |
||||
|
任务列表: |
||||
|
- 负责人: "[实际负责人]" |
||||
|
工作内容: "[实际工作内容]" |
||||
|
关联日报: |
||||
|
- 项目名称: "[日报项目名称]" |
||||
|
日报日期: "[日报日期]" |
||||
|
负责人: "[日报负责人]" |
||||
|
工作内容: "[日报工作内容]" |
||||
|
日报工时: [日报工时数值] |
||||
|
完成进度: [完成进度百分比] |
||||
|
- 项目名称: "[日报项目名称]" |
||||
|
日报日期: "[日报日期]" |
||||
|
负责人: "[日报负责人]" |
||||
|
工作内容: "[日报工作内容]" |
||||
|
日报工时: [日报工时数值] |
||||
|
完成进度: [完成进度百分比] |
||||
|
- 负责人: "[实际负责人]" |
||||
|
工作内容: "[实际工作内容]" |
||||
|
关联日报: [] |
||||
|
- 项目名称: "[实际项目名称2]" |
||||
|
任务列表: |
||||
|
- 负责人: "[实际负责人]" |
||||
|
工作内容: "[实际工作内容]" |
||||
|
关联日报: [] |
||||
|
- 项目名称: "临时任务" |
||||
|
任务列表: |
||||
|
- 负责人: "相关人员" |
||||
|
工作内容: "未关联的临时工作" |
||||
|
关联日报: |
||||
|
- 项目名称: "[日报项目名称]" |
||||
|
日报日期: "[日报日期]" |
||||
|
负责人: "[日报负责人]" |
||||
|
工作内容: "[日报工作内容]" |
||||
|
日报工时: [日报工时数值] |
||||
|
完成进度: [完成进度百分比] |
||||
|
- 项目名称: "[日报项目名称]" |
||||
|
日报日期: "[日报日期]" |
||||
|
负责人: "[日报负责人]" |
||||
|
工作内容: "[日报工作内容]" |
||||
|
日报工时: [日报工时数值] |
||||
|
完成进度: [完成进度百分比] |
||||
|
|
||||
|
### 字段说明: |
||||
|
- 关联日报字段:数组类型,包含匹配的日报完整数据 |
||||
|
- 临时任务项目:固定项目名称"临时任务",负责人固定为"相关人员" |
||||
|
- 空关联日报:如果没有匹配的日报,关联日报字段为空数组 |
||||
|
- 数据完整性:必须包含所有原始周计划任务,即使没有关联日报 |
||||
|
|
||||
|
### 输入数据: |
||||
|
{dataOrganizationResult} |
||||
|
|
||||
|
### 输出要求: |
||||
|
严格按照上述完整数据结构输出关联后的数据,确保: |
||||
|
1. 所有周计划任务都被保留 |
||||
|
2. 关联日报数据完整包含所有字段 |
||||
|
3. 临时任务项目位于项目列表末尾 |
||||
|
4. 保持清晰的文本层级格式 |
||||
|
5. 不遗漏任何日报数据 |
||||
|
"""; |
||||
|
|
||||
|
// 创建PromptTemplate并添加变量 |
||||
|
PromptTemplate template = new PromptTemplate(promptTemplate); |
||||
|
template.add("dataOrganizationResult", dataOrganizationResult); |
||||
|
|
||||
|
String prompt = template.render(); |
||||
|
|
||||
|
// 模型调用 |
||||
|
String content = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.content(); |
||||
|
|
||||
|
// 把结果存入 state |
||||
|
return Map.of("dataAssociationResult", content); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
package com.chenhai.chenhaiai.node; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-11-28 16:09 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class DataOrganizationNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public DataOrganizationNode(ChatClient.Builder builder) { |
||||
|
this.chatClient = builder.build(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 周计划和日报数据 |
||||
|
String weekPlanAndPersonalDaily = state.value("weekPlanAndPersonalDaily", ""); |
||||
|
|
||||
|
String promptTemplate = """ |
||||
|
你是一个数据格式转换助手,需要将已经翻译成中文的JSON数据转换为层级分明的文本格式,只保留关键信息。 |
||||
|
|
||||
|
### 任务要求: |
||||
|
1. 将中文JSON数据转换为文本格式 |
||||
|
2. 只保留指定的关键字段 |
||||
|
3. 保持项目列表和任务列表的完整层级结构 |
||||
|
4. 不进行任何数据分析 |
||||
|
|
||||
|
### 保留字段规则: |
||||
|
|
||||
|
【周计划数据保留字段】 |
||||
|
- 部门名称 |
||||
|
- 周次显示 |
||||
|
- 年份、月份、月中周次 |
||||
|
- 周开始日期、周结束日期 |
||||
|
- 项目列表(完整保留,包括下面的任务列表) |
||||
|
- 项目名称 |
||||
|
- 任务列表(完整保留) |
||||
|
- 负责人 |
||||
|
- 工作内容 |
||||
|
- 督办状态 |
||||
|
- 备注 |
||||
|
|
||||
|
【日报数据保留字段】 |
||||
|
- 部门名称 |
||||
|
- 项目名称 |
||||
|
- 日报日期 |
||||
|
- 负责人 |
||||
|
- 工作内容 |
||||
|
- 日报工时 |
||||
|
- 完成进度 |
||||
|
|
||||
|
### 过滤字段: |
||||
|
除上述指定字段外的所有其他字段 |
||||
|
|
||||
|
### 输入数据:{weekPlanAndPersonalDaily} |
||||
|
|
||||
|
### 输出要求: |
||||
|
直接输出精简后的层级文本格式,严格按照指定的字段保留。 |
||||
|
"""; |
||||
|
|
||||
|
PromptTemplate template = new PromptTemplate(promptTemplate); |
||||
|
template.add("weekPlanAndPersonalDaily", weekPlanAndPersonalDaily); |
||||
|
String prompt = template.render(); |
||||
|
|
||||
|
// 模型调用 |
||||
|
String content = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.content(); |
||||
|
|
||||
|
// 把句子存入 state |
||||
|
return Map.of("dataOrganizationResult", content); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,167 @@ |
|||||
|
package com.chenhai.chenhaiai.node; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-11-28 16:09 |
||||
|
*/ |
||||
|
public class DataTranslationNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public DataTranslationNode(ChatClient.Builder builder) { |
||||
|
this.chatClient = builder.build(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 周计划和日报数据 |
||||
|
String weekPlan = state.value("weekPlan", ""); |
||||
|
String personalDaily = state.value("personalDaily", ""); |
||||
|
|
||||
|
String promptTemplate = """ |
||||
|
你是一个JSON字段翻译助手,需要将周计划数据和日报数据合并成一个完整的JSON,并将所有字段名翻译为中文。 |
||||
|
|
||||
|
### 任务要求: |
||||
|
1. 将周计划JSON和日报JSON合并为一个完整JSON |
||||
|
2. 将所有字段名(key)翻译为对应的中文名称 |
||||
|
3. 保持原有的数据结构和数值不变 |
||||
|
4. 不进行任何数据关联分析,只做纯粹的字段翻译和结构合并 |
||||
|
|
||||
|
### 字段翻译对照表: |
||||
|
|
||||
|
【周计划字段翻译】 |
||||
|
- "total" → "总数" |
||||
|
- "rows" → "数据行" |
||||
|
- "code" → "状态码" |
||||
|
- "msg" → "消息" |
||||
|
- "createBy" → "创建人" |
||||
|
- "createTime" → "创建时间" |
||||
|
- "updateBy" → "更新人" |
||||
|
- "updateTime" → "更新时间" |
||||
|
- "remark" → "备注" |
||||
|
- "id" → "主键ID" |
||||
|
- "deptId" → "部门ID" |
||||
|
- "deptName" → "部门名称" |
||||
|
- "schedule" → "计划进度" |
||||
|
- "weekDisplay" → "周次显示" |
||||
|
- "year" → "年份" |
||||
|
- "month" → "月份" |
||||
|
- "weekOfMonth" → "月中周次" |
||||
|
- "weekStartDate" → "周开始日期" |
||||
|
- "weekEndDate" → "周结束日期" |
||||
|
- "monitoringStatus" → "督办状态" |
||||
|
- "projectList" → "项目列表" |
||||
|
- "projectNote" → "项目备注" |
||||
|
- "projectName" → "项目名称" |
||||
|
- "tasks" → "任务列表" |
||||
|
- "note" → "备注" |
||||
|
- "assistant" → "协助人" |
||||
|
- "developer" → "负责人" |
||||
|
- "superviseStatus" → "督办状态" |
||||
|
- "mainId" → "主表ID" |
||||
|
- "planStartDate" → "预计开始时间" |
||||
|
- "planEndDate" → "预计结束时间" |
||||
|
- "planHours" → "预计用时" |
||||
|
- "content" → "工作内容" |
||||
|
|
||||
|
【日报字段翻译】 |
||||
|
- "projectId" → "项目ID" |
||||
|
- "guanlianUids" → "关联用户ID" |
||||
|
- "beizhu" → "备注信息" |
||||
|
- "workList" → "工作列表" |
||||
|
- "isPlan" → "是否计划内" |
||||
|
- "dailyPaperDate" → "日报日期" |
||||
|
- "dailyPaperType" → "日报类型" |
||||
|
- "dailyPaperStatus" → "日报状态" |
||||
|
- "dailyPaperSort" → "日报排序" |
||||
|
- "dailyPaperHour" → "日报工时" |
||||
|
- "itemSchedule" → "单项完成进度" |
||||
|
- "itemStaus" → "单项状态" |
||||
|
- "filePath" → "文件路径" |
||||
|
- "userIds" → "用户ID集合" |
||||
|
- "images" → "图片集合" |
||||
|
- "delFlag" → "删除标志" |
||||
|
- "other" → "其他信息" |
||||
|
- "jsondata" → "JSON数据" |
||||
|
- "taskId" → "任务ID" |
||||
|
- "completeStatus" → "完成状态" |
||||
|
- "delItem" → "删除项" |
||||
|
- "level" → "优先级" |
||||
|
- "rejectedStatus" → "驳回状态" |
||||
|
- "rejectedReason" → "驳回原因" |
||||
|
- "rejectedUserId" → "驳回人ID" |
||||
|
|
||||
|
### 输出结构要求: |
||||
|
输出一个包含两个主要字段的JSON对象: |
||||
|
- "周计划数据": 包含翻译后的周计划数据 |
||||
|
- "日报数据": 包含翻译后的日报数据 |
||||
|
|
||||
|
### 输入数据: |
||||
|
周计划数据: |
||||
|
{weekPlan} |
||||
|
|
||||
|
日报数据: |
||||
|
{personalDaily} |
||||
|
|
||||
|
### 输出要求: |
||||
|
直接输出合并后的完整JSON,所有字段名使用中文,保持数据结构完整。不要添加任何额外的解释。 |
||||
|
"""; |
||||
|
|
||||
|
// 使用 PromptTemplate 进行变量替换 |
||||
|
PromptTemplate template = new PromptTemplate(promptTemplate); |
||||
|
template.add("weekPlan", weekPlan); |
||||
|
template.add("personalDaily", personalDaily); |
||||
|
String prompt = template.render(); |
||||
|
|
||||
|
// 模型调用 |
||||
|
String content = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.content(); |
||||
|
// 清理输出,确保是纯净的JSON |
||||
|
String cleanJson = cleanJsonOutput(content); |
||||
|
// 把结果存入 state |
||||
|
return Map.of("weekPlanAndPersonalDaily", cleanJson); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 清理JSON输出,移除可能的额外文本 |
||||
|
*/ |
||||
|
private String cleanJsonOutput(String rawOutput) { |
||||
|
if (rawOutput == null) { |
||||
|
return "{}"; |
||||
|
} |
||||
|
|
||||
|
String cleaned = rawOutput.trim(); |
||||
|
|
||||
|
// 移除代码块标记 |
||||
|
cleaned = cleaned.replaceAll("```json", "").replaceAll("```", ""); |
||||
|
|
||||
|
// 提取第一个 { 和最后一个 } 之间的内容 |
||||
|
int startIndex = cleaned.indexOf('{'); |
||||
|
int endIndex = cleaned.lastIndexOf('}'); |
||||
|
|
||||
|
if (startIndex >= 0 && endIndex > startIndex) { |
||||
|
cleaned = cleaned.substring(startIndex, endIndex + 1); |
||||
|
} |
||||
|
|
||||
|
// 验证是否是有效的JSON格式 |
||||
|
try { |
||||
|
// 简单的格式验证 |
||||
|
if (cleaned.startsWith("{") && cleaned.endsWith("}")) { |
||||
|
return cleaned; |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
// 格式有问题,返回原始内容 |
||||
|
} |
||||
|
|
||||
|
return cleaned; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
package com.chenhai.chenhaiai.node; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-11-28 16:09 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class WeekPlanAnalysisNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public WeekPlanAnalysisNode(ChatClient.Builder builder) { |
||||
|
this.chatClient = builder.build(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 周计划和日报数据 |
||||
|
String dataAssociationResult = state.value("dataAssociationResult", ""); |
||||
|
|
||||
|
|
||||
|
String optimizedPrompt = """ |
||||
|
你是一个专业的项目管理分析师。请基于提供的周计划和日报关联数据,进行工作效率分析。 |
||||
|
|
||||
|
## 输入数据: |
||||
|
{dataAssociationResult} |
||||
|
|
||||
|
## 分析要求: |
||||
|
请按照以下结构化格式输出分析结果: |
||||
|
|
||||
|
### 📊 核心指标概览 |
||||
|
- 总体完成率: [计算具体百分比] |
||||
|
- 总工时对比: 计划[数字]h vs 实际[数字]h |
||||
|
- 成员工作量分布: [简要描述] |
||||
|
|
||||
|
### 👥 团队效能分析 |
||||
|
**成员表现排名:** |
||||
|
1. [姓名] - [任务数]项, [工时]h |
||||
|
2. [姓名] - [任务数]项, [工时]h |
||||
|
|
||||
|
**负荷均衡度:** [均衡/需要优化] |
||||
|
|
||||
|
### ⚠️ 风险与问题 |
||||
|
- 进度风险: [具体描述] |
||||
|
- 资源问题: [具体描述] |
||||
|
- 其他风险: [具体描述] |
||||
|
|
||||
|
### 💡 改进建议 |
||||
|
**短期建议 (本周):** |
||||
|
- [具体可执行建议1] |
||||
|
- [具体可执行建议2] |
||||
|
|
||||
|
**长期优化:** |
||||
|
- [战略性建议1] |
||||
|
- [战略性建议2] |
||||
|
|
||||
|
## 输出要求: |
||||
|
- 使用具体数据支撑每个结论 |
||||
|
- 避免空洞描述,提供可量化指标 |
||||
|
- 建议要具体可执行 |
||||
|
- 风险描述要明确具体 |
||||
|
"""; |
||||
|
|
||||
|
PromptTemplate promptTemplate = new PromptTemplate(optimizedPrompt); |
||||
|
promptTemplate.add("dataAssociationResult", dataAssociationResult); |
||||
|
String prompt = promptTemplate.render(); |
||||
|
// 模型调用 |
||||
|
String content = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.content(); |
||||
|
|
||||
|
// 把句子存入 state |
||||
|
return Map.of("result",content); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,139 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-05 17:58 |
||||
|
*/ |
||||
|
public class Analysis implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
private final ObjectMapper objectMapper; |
||||
|
|
||||
|
public Analysis(ChatClient chatClient) { |
||||
|
this.chatClient = chatClient; |
||||
|
this.objectMapper = new ObjectMapper(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 输入数据 |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
|
||||
|
// 将对象转为 JSON 字符串 |
||||
|
String jsonData; |
||||
|
try { |
||||
|
jsonData = objectMapper.writeValueAsString(weekPlanResponse); |
||||
|
} catch (Exception e) { |
||||
|
jsonData = "数据序列化失败,请检查WeekPlanResponse结构: " + e.getMessage(); |
||||
|
} |
||||
|
|
||||
|
// 提示词 |
||||
|
String prompt = """ |
||||
|
请作为各部门工作效能分析师,基于以下完整的周度工作数据进行分析。 |
||||
|
数据格式为weekPlanResponse对象,包含planDetails(周计划)和dailyPapers(日报)。 |
||||
|
|
||||
|
【原始数据】 |
||||
|
""" + jsonData + """ |
||||
|
|
||||
|
【分析要求】 |
||||
|
请按以下6个模块顺序进行流式分析输出,每个模块用---分隔: |
||||
|
|
||||
|
--- |
||||
|
[模块1: 核心数据快照] |
||||
|
分析周期:{请从数据中提取周次信息} |
||||
|
部门:{请从数据中提取部门名称} |
||||
|
计划任务数:{请计算planDetails数量} |
||||
|
日报记录数:{请计算dailyPapers数量} |
||||
|
参与人员:{请从数据中提取所有参与人员姓名} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块2: 计划完成度分析] |
||||
|
**统计结果**: |
||||
|
✅ 已完成/有实质进展:{请分析并统计已完成项}项 |
||||
|
🔄 进行中但未完成:{请分析并统计进行中项}项 |
||||
|
⏸️ 计划但无进展:{请分析并统计无进展项}项 |
||||
|
📋 完全未开始:{请分析并统计未开始项}项 |
||||
|
|
||||
|
**完成率计算**:{请计算(已完成+进行中)/总数百分比}% |
||||
|
|
||||
|
**关键发现**:{请简要说明最突出的计划执行问题} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块3: 人员工作负荷分析] |
||||
|
**工时统计**: |
||||
|
{请从dailyPapers统计每人总工时} |
||||
|
|
||||
|
**角色与效能评估**: |
||||
|
{请为每个成员分析:主要工作领域、计划内外工时比、效能状态} |
||||
|
|
||||
|
**负荷评级**:{请基于人均日工时评估:高/中/低} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块4: 工作重心与干扰分析] |
||||
|
**本周实际工作重心**: |
||||
|
1. {请基于日报归纳的第一重心} |
||||
|
2. {请基于日报归纳的第二重心} |
||||
|
|
||||
|
**主要干扰源**: |
||||
|
• {请识别最主要的计划外工作类型} |
||||
|
• {请识别最耗时的非开发活动} |
||||
|
|
||||
|
**计划vs实际对比**:{请说明计划任务与实际投入的匹配程度} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块5: 风险识别与协作评估] |
||||
|
**技术风险**: |
||||
|
⚠️ {请从planDetails的note字段识别技术风险} |
||||
|
|
||||
|
**管理风险**: |
||||
|
⚠️ {请分析跨部门需求处理流程问题} |
||||
|
⚠️ {请分析历史项目维护对计划的冲击} |
||||
|
|
||||
|
**协作亮点**:{请从日报中发现的有效协作模式} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块6: 改进建议与下周关注] |
||||
|
**核心结论**:{请用一句话总结本周效能核心问题} |
||||
|
|
||||
|
**具体建议**: |
||||
|
🔥 立即行动:{请提出一项可快速执行的具体改进} |
||||
|
📈 本周优化:{请提出一项需要协调的流程优化} |
||||
|
📅 长期规划:{请提出一项战略性建议} |
||||
|
|
||||
|
**下周重点关注**:{请基于本周进展,提出下周需特别关注的任务} |
||||
|
--- |
||||
|
|
||||
|
【输出规则】 |
||||
|
1. 严格按6个模块顺序输出,每个模块以---开始和结束 |
||||
|
2. 每个模块内容控制在3-5行,便于流式显示 |
||||
|
3. 关键数据用**加粗**,如**完成率:45%** |
||||
|
4. 使用简洁的项目符号•或数字列表 |
||||
|
5. 所有{...}占位符请替换为实际分析结果 |
||||
|
6. 分析要基于具体数据,避免空泛描述 |
||||
|
7. 如果数据不足或缺失,请说明"数据不足,无法分析" |
||||
|
"""; |
||||
|
|
||||
|
// 模型调用 |
||||
|
String result = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.content(); |
||||
|
|
||||
|
System.out.println("==========================result:==================================\n" + result); |
||||
|
|
||||
|
// 把结果存入 state |
||||
|
return Map.of("result", result); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,148 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.GraphResponse; |
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.alibaba.cloud.ai.graph.streaming.StreamingOutput; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.model.ChatResponse; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import reactor.core.publisher.Flux; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 流式分析节点 - 完全按照TranslateNode的模式 |
||||
|
*/ |
||||
|
public class AnalysisStreamNode implements NodeAction { |
||||
|
|
||||
|
private final ChatClient chatClient; |
||||
|
private final ObjectMapper objectMapper; |
||||
|
|
||||
|
public AnalysisStreamNode(ChatClient chatClient) { |
||||
|
this.chatClient = chatClient; |
||||
|
this.objectMapper = new ObjectMapper(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) { |
||||
|
// 从state中获取输入数据 |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
|
||||
|
// 将对象转为JSON字符串 |
||||
|
String jsonData; |
||||
|
try { |
||||
|
jsonData = objectMapper.writeValueAsString(weekPlanResponse); |
||||
|
} catch (Exception e) { |
||||
|
jsonData = "数据序列化失败: " + e.getMessage(); |
||||
|
} |
||||
|
|
||||
|
// 提示词(保持原有格式,但稍微简化) |
||||
|
String prompt = """ |
||||
|
请作为各部门工作效能分析师,基于以下完整的周度工作数据进行分析。 |
||||
|
数据格式为weekPlanResponse对象,包含planDetails(周计划)和dailyPapers(日报)。 |
||||
|
|
||||
|
【原始数据】 |
||||
|
""" + jsonData + """ |
||||
|
|
||||
|
【分析要求】 |
||||
|
请按以下6个模块顺序进行流式分析输出,每个模块用---分隔: |
||||
|
|
||||
|
--- |
||||
|
[模块1: 核心数据快照] |
||||
|
分析周期:{请从数据中提取周次信息} |
||||
|
部门:{请从数据中提取部门名称} |
||||
|
计划任务数:{请计算planDetails数量} |
||||
|
日报记录数:{请计算dailyPapers数量} |
||||
|
参与人员:{请从数据中提取所有参与人员姓名} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块2: 计划完成度分析] |
||||
|
**统计结果**: |
||||
|
✅ 已完成/有实质进展:{请分析并统计已完成项}项 |
||||
|
🔄 进行中但未完成:{请分析并统计进行中项}项 |
||||
|
⏸️ 计划但无进展:{请分析并统计无进展项}项 |
||||
|
📋 完全未开始:{请分析并统计未开始项}项 |
||||
|
|
||||
|
**完成率计算**:{请计算(已完成+进行中)/总数百分比}% |
||||
|
|
||||
|
**关键发现**:{请简要说明最突出的计划执行问题} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块3: 人员工作负荷分析] |
||||
|
**工时统计**: |
||||
|
{请从dailyPapers统计每人总工时} |
||||
|
|
||||
|
**角色与效能评估**: |
||||
|
{请为每个成员分析:主要工作领域、计划内外工时比、效能状态} |
||||
|
|
||||
|
**负荷评级**:{请基于人均日工时评估:高/中/低} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块4: 工作重心与干扰分析] |
||||
|
**本周实际工作重心**: |
||||
|
1. {请基于日报归纳的第一重心} |
||||
|
2. {请基于日报归纳的第二重心} |
||||
|
|
||||
|
**主要干扰源**: |
||||
|
• {请识别最主要的计划外工作类型} |
||||
|
• {请识别最耗时的非开发活动} |
||||
|
|
||||
|
**计划vs实际对比**:{请说明计划任务与实际投入的匹配程度} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块5: 风险识别与协作评估] |
||||
|
**技术风险**: |
||||
|
⚠️ {请从planDetails的note字段识别技术风险} |
||||
|
|
||||
|
**管理风险**: |
||||
|
⚠️ {请分析跨部门需求处理流程问题} |
||||
|
⚠️ {请分析历史项目维护对计划的冲击} |
||||
|
|
||||
|
**协作亮点**:{请从日报中发现的有效协作模式} |
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
[模块6: 改进建议与下周关注] |
||||
|
**核心结论**:{请用一句话总结本周效能核心问题} |
||||
|
|
||||
|
**具体建议**: |
||||
|
🔥 立即行动:{请提出一项可快速执行的具体改进} |
||||
|
📈 本周优化:{请提出一项需要协调的流程优化} |
||||
|
📅 长期规划:{请提出一项战略性建议} |
||||
|
|
||||
|
**下周重点关注**:{请基于本周进展,提出下周需特别关注的任务} |
||||
|
--- |
||||
|
|
||||
|
【输出规则】 |
||||
|
1. 严格按6个模块顺序输出,每个模块以---开始和结束 |
||||
|
2. 每个模块内容控制在3-5行,便于流式显示 |
||||
|
3. 关键数据用**加粗**,如**完成率:45%** |
||||
|
4. 使用简洁的项目符号•或数字列表 |
||||
|
5. 所有{...}占位符请替换为实际分析结果 |
||||
|
6. 分析要基于具体数据,避免空泛描述 |
||||
|
7. 如果数据不足或缺失,请说明"数据不足,无法分析" |
||||
|
"""; |
||||
|
|
||||
|
try { |
||||
|
// 模型调用 - 获取 Flux<String> |
||||
|
Flux<String> chatResponseFlux = this.chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.stream() |
||||
|
.content(); |
||||
|
|
||||
|
// 在 1.0.0.0 版本中,我们需要将 Flux 转换为 AsyncGenerator |
||||
|
// 或者直接返回 Flux,让框架处理 |
||||
|
return Map.of("analysis_result", chatResponseFlux); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
// 错误处理 |
||||
|
return Map.of("analysis_result", Flux.just("分析失败: " + e.getMessage())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.jdbc; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.DailyPaper; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import org.springframework.jdbc.core.BeanPropertyRowMapper; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class DailyPaperJdbcNode implements NodeAction { |
||||
|
private final JdbcTemplate jdbcTemplate; |
||||
|
private final ProgressEmitter progressEmitter; |
||||
|
|
||||
|
public DailyPaperJdbcNode(ProgressEmitter progressEmitter) { |
||||
|
this.progressEmitter = progressEmitter; |
||||
|
// 直接在代码中设置数据库连接参数 |
||||
|
String url = "jdbc:mysql://172.16.1.121:3306/erp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; |
||||
|
String username = "erperp"; |
||||
|
String password = "HeAmK7TBTMDcerpj2"; |
||||
|
|
||||
|
DriverManagerDataSource dataSource = new DriverManagerDataSource(); |
||||
|
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); |
||||
|
dataSource.setUrl(url); |
||||
|
dataSource.setUsername(username); |
||||
|
dataSource.setPassword(password); |
||||
|
|
||||
|
this.jdbcTemplate = new JdbcTemplate(dataSource); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
|
||||
|
progressEmitter.emitProgress("DailyPaperJdbcNode", "正在查询日报记录..."); |
||||
|
|
||||
|
// 直接查询 |
||||
|
String sql = """ |
||||
|
SELECT |
||||
|
project_name as projectName, |
||||
|
content, |
||||
|
DATE_FORMAT(daily_paper_date, '%Y-%m-%d') as dailyPaperDate, |
||||
|
daily_paper_hour as dailyPaperHour |
||||
|
FROM ch_rb_urecord |
||||
|
WHERE dept_id = ? |
||||
|
AND daily_paper_date >= ? |
||||
|
AND daily_paper_date <= ? |
||||
|
ORDER BY daily_paper_date |
||||
|
"""; |
||||
|
|
||||
|
List<DailyPaper> dailyPapers = jdbcTemplate.query(sql, |
||||
|
new BeanPropertyRowMapper<>(DailyPaper.class), |
||||
|
weekPlanResponse.getDept().getDeptId(), |
||||
|
weekPlanResponse.getPlanMain().getWeekStartDate().substring(0, 10), |
||||
|
weekPlanResponse.getPlanMain().getWeekEndDate().substring(0, 10) |
||||
|
); |
||||
|
|
||||
|
progressEmitter.emitProgress("DailyPaperJdbcNode", "查询到 " + dailyPapers.size() + " 条日报记录"); |
||||
|
|
||||
|
System.out.println("查询到 " + dailyPapers.size() + " 条日报记录"); |
||||
|
weekPlanResponse.setDailyPapers(dailyPapers); |
||||
|
|
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.jdbc; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.DailyPaper; |
||||
|
import com.chenhai.chenhaiai.entity.Dept; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.jdbc.core.BeanPropertyRowMapper; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class DeptJdbcNode implements NodeAction { |
||||
|
private final JdbcTemplate jdbcTemplate; |
||||
|
private final ProgressEmitter progressEmitter; |
||||
|
|
||||
|
public DeptJdbcNode(ProgressEmitter progressEmitter) { |
||||
|
this.progressEmitter = progressEmitter; |
||||
|
// 直接在代码中设置数据库连接参数 |
||||
|
String url = "jdbc:mysql://172.16.1.121:3306/erp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; |
||||
|
String username = "erperp"; |
||||
|
String password = "HeAmK7TBTMDcerpj2"; |
||||
|
|
||||
|
DriverManagerDataSource dataSource = new DriverManagerDataSource(); |
||||
|
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); |
||||
|
dataSource.setUrl(url); |
||||
|
dataSource.setUsername(username); |
||||
|
dataSource.setPassword(password); |
||||
|
|
||||
|
this.jdbcTemplate = new JdbcTemplate(dataSource); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
|
||||
|
// 实时推送进度 - 在执行前 |
||||
|
progressEmitter.emitProgress("DeptJdbcNode", "开始查询部门数据: " + weekPlanResponse.getDeptName()); |
||||
|
|
||||
|
// 直接查询 |
||||
|
String sql = """ |
||||
|
select dept_id,dept_name from sys_dept where dept_name = ? |
||||
|
"""; |
||||
|
|
||||
|
Dept dept = jdbcTemplate.queryForObject(sql, |
||||
|
new BeanPropertyRowMapper<>(Dept.class), |
||||
|
weekPlanResponse.getDeptName() |
||||
|
); |
||||
|
|
||||
|
// 查询完成,实时推送 |
||||
|
progressEmitter.emitProgress("DeptJdbcNode", |
||||
|
"✅ 查询到部门: " + dept.getDeptName() + " (ID: " + dept.getDeptId() + ")"); |
||||
|
|
||||
|
System.out.println("查询到 " + dept.getDeptName() + " 的部门数据"); |
||||
|
weekPlanResponse.setDept(dept); |
||||
|
|
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,128 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.jdbc; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.entity.git.GitAnalysisData; |
||||
|
import com.chenhai.chenhaiai.service.GiteaAnalysisService; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
import java.time.LocalDateTime; |
||||
|
import java.time.LocalTime; |
||||
|
import java.time.ZoneId; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.CompletableFuture; |
||||
|
|
||||
|
public class GitAnalysisNode implements NodeAction { |
||||
|
private final ProgressEmitter progressEmitter; |
||||
|
private final GiteaAnalysisService giteaAnalysisService; |
||||
|
|
||||
|
public GitAnalysisNode(ProgressEmitter progressEmitter, GiteaAnalysisService giteaAnalysisService) { |
||||
|
this.progressEmitter = progressEmitter; |
||||
|
this.giteaAnalysisService = giteaAnalysisService; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
Boolean isResearchDept = state.value("isResearchDept", false); |
||||
|
|
||||
|
if (!Boolean.TRUE.equals(isResearchDept)) { |
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "⏭️ 非研发部,跳过Git分析"); |
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
|
||||
|
// 实时推送各个阶段 |
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "💾 开始Git分析..."); |
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "📡 连接Gitea服务..."); |
||||
|
|
||||
|
String weekStartDate = weekPlanResponse.getPlanMain().getWeekStartDate(); |
||||
|
String weekEndDate = weekPlanResponse.getPlanMain().getWeekEndDate(); |
||||
|
|
||||
|
// 转换日期时间为ISO格式 |
||||
|
String since = convertToIsoDateTime(weekStartDate, true); |
||||
|
String until = convertToIsoDateTime(weekEndDate, false); |
||||
|
|
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "🔍 查询Git提交: " + weekStartDate + " 至 " + weekEndDate); |
||||
|
|
||||
|
try { |
||||
|
// 同步获取Git分析结果(阻塞等待) |
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "⏳ 正在分析Git提交数据..."); |
||||
|
|
||||
|
// 调用异步方法并同步等待结果 |
||||
|
GitAnalysisData gitAnalysisData = giteaAnalysisService.analyzeGitDataAsync(since, until).get(); |
||||
|
|
||||
|
// 设置到响应中 |
||||
|
weekPlanResponse.setGitAnalysis(gitAnalysisData); |
||||
|
|
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "✅ Git分析完成"); |
||||
|
} catch (Exception e) { |
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "❌ Git分析失败: " + e.getMessage()); |
||||
|
|
||||
|
// 创建一个包含错误信息的GitAnalysisData |
||||
|
GitAnalysisData errorData = new GitAnalysisData(); |
||||
|
errorData.setBasicInfo(new com.chenhai.chenhaiai.entity.git.BasicInfo( |
||||
|
since + " 至 " + until, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
"分析失败: " + e.getMessage() |
||||
|
)); |
||||
|
errorData.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
|
||||
|
weekPlanResponse.setGitAnalysis(errorData); |
||||
|
} |
||||
|
|
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
|
||||
|
private String convertToIsoDateTime(String dateStr, boolean isStart) { |
||||
|
try { |
||||
|
// 简化处理:假设日期格式为 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss |
||||
|
String datePart = dateStr.trim(); |
||||
|
if (datePart.contains(" ")) { |
||||
|
datePart = datePart.substring(0, 10); |
||||
|
} |
||||
|
|
||||
|
// 解析日期 |
||||
|
LocalDate date = LocalDate.parse( |
||||
|
datePart, |
||||
|
DateTimeFormatter.ISO_DATE |
||||
|
); |
||||
|
|
||||
|
// 设置时间 |
||||
|
LocalDateTime dateTime; |
||||
|
if (isStart) { |
||||
|
dateTime = date.atTime(LocalTime.MIN); // 00:00:00 |
||||
|
} else { |
||||
|
dateTime = date.atTime(LocalTime.MAX); // 23:59:59.999999999 |
||||
|
} |
||||
|
|
||||
|
// 转换为带时区的ISO格式 |
||||
|
return dateTime.atZone(ZoneId.systemDefault()) |
||||
|
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
// 如果失败,返回合理的默认值(上周时间范围) |
||||
|
progressEmitter.emitProgress("GitAnalysisNode", "⚠️ 日期转换失败,使用上周时间范围: " + e.getMessage()); |
||||
|
|
||||
|
LocalDate lastMonday = LocalDate.now() |
||||
|
.minusWeeks(1) |
||||
|
.with(java.time.DayOfWeek.MONDAY); |
||||
|
|
||||
|
LocalDateTime result; |
||||
|
if (isStart) { |
||||
|
result = lastMonday.atStartOfDay(); |
||||
|
} else { |
||||
|
result = lastMonday.plusDays(6).atTime(LocalTime.MAX); |
||||
|
} |
||||
|
|
||||
|
return result.atZone(ZoneId.systemDefault()) |
||||
|
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.jdbc; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.UserInfo; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import org.springframework.jdbc.core.BeanPropertyRowMapper; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class UserJdbcNode implements NodeAction { |
||||
|
private final JdbcTemplate jdbcTemplate; |
||||
|
private final ProgressEmitter progressEmitter; |
||||
|
|
||||
|
public UserJdbcNode(ProgressEmitter progressEmitter) { |
||||
|
this.progressEmitter = progressEmitter; |
||||
|
// 直接在代码中设置数据库连接参数 |
||||
|
String url = "jdbc:mysql://172.16.1.121:3306/erp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; |
||||
|
String username = "erperp"; |
||||
|
String password = "HeAmK7TBTMDcerpj2"; |
||||
|
|
||||
|
DriverManagerDataSource dataSource = new DriverManagerDataSource(); |
||||
|
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); |
||||
|
dataSource.setUrl(url); |
||||
|
dataSource.setUsername(username); |
||||
|
dataSource.setPassword(password); |
||||
|
|
||||
|
this.jdbcTemplate = new JdbcTemplate(dataSource); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
Long deptId = weekPlanResponse.getDept().getDeptId(); |
||||
|
|
||||
|
// 实时推送 |
||||
|
progressEmitter.emitProgress("UserJdbcNode", "开始查询部门人员,部门ID: " + deptId); |
||||
|
|
||||
|
// 直接查询 |
||||
|
String sql = """ |
||||
|
select user_id, user_name from sys_user where dept_id = ? |
||||
|
"""; |
||||
|
|
||||
|
List<UserInfo> userInfoList = jdbcTemplate.query(sql, |
||||
|
new BeanPropertyRowMapper<>(UserInfo.class), |
||||
|
deptId |
||||
|
); |
||||
|
|
||||
|
// 实时推送完成 |
||||
|
progressEmitter.emitProgress("UserJdbcNode", "✅ 查询到 " + userInfoList.size() + " 个用户数据"); |
||||
|
|
||||
|
System.out.println("查询到 " + userInfoList.size() + " 个用户数据"); |
||||
|
weekPlanResponse.setUserInfos(userInfoList); |
||||
|
|
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.jdbc; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.UserInfo; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanDetail; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.core.ParameterizedTypeReference; |
||||
|
import org.springframework.jdbc.core.BeanPropertyRowMapper; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class WeekPlanDetailJdbcNode implements NodeAction { |
||||
|
private final JdbcTemplate jdbcTemplate; |
||||
|
private final ProgressEmitter progressEmitter; |
||||
|
|
||||
|
public WeekPlanDetailJdbcNode(ProgressEmitter progressEmitter) { |
||||
|
this.progressEmitter = progressEmitter; |
||||
|
// 直接在代码中设置数据库连接参数 |
||||
|
String url = "jdbc:mysql://172.16.1.121:3306/erp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; |
||||
|
String username = "erperp"; |
||||
|
String password = "HeAmK7TBTMDcerpj2"; |
||||
|
|
||||
|
DriverManagerDataSource dataSource = new DriverManagerDataSource(); |
||||
|
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); |
||||
|
dataSource.setUrl(url); |
||||
|
dataSource.setUsername(username); |
||||
|
dataSource.setPassword(password); |
||||
|
|
||||
|
this.jdbcTemplate = new JdbcTemplate(dataSource); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
Long mainId = weekPlanResponse.getPlanMain().getId(); |
||||
|
|
||||
|
progressEmitter.emitProgress("WeekPlanDetailJdbcNode", "正在查询周计划详情数据..."); |
||||
|
|
||||
|
// 直接查询 |
||||
|
String sql = """ |
||||
|
select project_name, content, developer, supervise_status, note, project_note from ch_week_project where main_id = ? |
||||
|
"""; |
||||
|
|
||||
|
List<WeekPlanDetail> weekPlanDetailList = jdbcTemplate.query(sql, |
||||
|
new BeanPropertyRowMapper<>(WeekPlanDetail.class), |
||||
|
mainId |
||||
|
); |
||||
|
|
||||
|
progressEmitter.emitProgress("WeekPlanDetailJdbcNode", "查询到 " + weekPlanDetailList.size() + " 个周计划详情数据"); |
||||
|
|
||||
|
System.out.println("查询到 " + weekPlanDetailList.size() + " 个周计划详情数据"); |
||||
|
weekPlanResponse.setPlanDetails(weekPlanDetailList); |
||||
|
|
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.jdbc; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.Dept; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanMain; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.utils.ProgressEmitter; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.jdbc.core.BeanPropertyRowMapper; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class WeekPlanMainJdbcNode implements NodeAction { |
||||
|
private final JdbcTemplate jdbcTemplate; |
||||
|
private final ProgressEmitter progressEmitter; |
||||
|
|
||||
|
public WeekPlanMainJdbcNode(ProgressEmitter progressEmitter) { |
||||
|
this.progressEmitter = progressEmitter; |
||||
|
// 直接在代码中设置数据库连接参数 |
||||
|
String url = "jdbc:mysql://172.16.1.121:3306/erp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; |
||||
|
String username = "erperp"; |
||||
|
String password = "HeAmK7TBTMDcerpj2"; |
||||
|
|
||||
|
DriverManagerDataSource dataSource = new DriverManagerDataSource(); |
||||
|
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); |
||||
|
dataSource.setUrl(url); |
||||
|
dataSource.setUsername(username); |
||||
|
dataSource.setPassword(password); |
||||
|
|
||||
|
this.jdbcTemplate = new JdbcTemplate(dataSource); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
String deptName = weekPlanResponse.getDeptName(); |
||||
|
String weekDisplay = weekPlanResponse.getWeekDisplay(); |
||||
|
|
||||
|
progressEmitter.emitProgress("WeekPlanMainJdbcNode", "正在查询周计划数据是否存在..."); |
||||
|
|
||||
|
// 直接查询 |
||||
|
String sql = """ |
||||
|
SELECT id, dept_name, week_display, week_start_date, week_end_date |
||||
|
FROM ch_week_plan |
||||
|
WHERE dept_name = ? AND week_display = ? |
||||
|
"""; |
||||
|
|
||||
|
WeekPlanMain weekPlanMain = jdbcTemplate.queryForObject(sql, |
||||
|
new BeanPropertyRowMapper<>(WeekPlanMain.class), |
||||
|
deptName, |
||||
|
weekDisplay |
||||
|
); |
||||
|
|
||||
|
progressEmitter.emitProgress("WeekPlanMainJdbcNode", "查询到 " + weekPlanMain.getDeptName() + " 的周计划数据"); |
||||
|
|
||||
|
System.out.println("查询到 " + weekPlanMain.getDeptName() + " 的周计划数据"); |
||||
|
weekPlanResponse.setPlanMain(weekPlanMain); |
||||
|
|
||||
|
// 判断是否为研发部(关键修改) |
||||
|
boolean isResearchDept = "研发部".equals(deptName); |
||||
|
|
||||
|
return Map.of( |
||||
|
"weekPlanResponse", weekPlanResponse, |
||||
|
"isResearchDept", isResearchDept // 返回布尔值,而不是对象 |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.mcp; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.DailyPaper; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanMain; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
import org.springframework.ai.tool.ToolCallback; |
||||
|
import org.springframework.ai.tool.ToolCallbackProvider; |
||||
|
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; |
||||
|
import org.springframework.core.ParameterizedTypeReference; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class DailyPaperNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public DailyPaperNode(ChatClient chatClient) { |
||||
|
this.chatClient = chatClient; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 输入数据 |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
Long deptId = weekPlanResponse.getDept().getDeptId(); |
||||
|
String weekStartDate = weekPlanResponse.getPlanMain().getWeekStartDate().substring(0, 10); |
||||
|
String weekEndDate = weekPlanResponse.getPlanMain().getWeekEndDate().substring(0, 10); |
||||
|
|
||||
|
// 提示词 |
||||
|
String prompt = """ |
||||
|
请使用可用的工具执行以下SQL查询: |
||||
|
SELECT project_name, content, daily_paper_date, daily_paper_hour |
||||
|
FROM ch_rb_urecord |
||||
|
WHERE dept_id = %d |
||||
|
AND daily_paper_date >= '%s' |
||||
|
AND daily_paper_date <= '%s' |
||||
|
|
||||
|
**重要**:请确保返回完整的数据,如果数据量大,请分批处理但最终必须返回全部数据。 |
||||
|
""".formatted(deptId, weekStartDate, weekEndDate); |
||||
|
|
||||
|
ZhiPuAiChatOptions chatOptions = ZhiPuAiChatOptions.builder() |
||||
|
.maxTokens(15536) |
||||
|
.model("glm-4.5") |
||||
|
.build(); |
||||
|
|
||||
|
// 模型调用 |
||||
|
List<DailyPaper> dailyPaperList = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.options(chatOptions) |
||||
|
.call() |
||||
|
.entity(new ParameterizedTypeReference<List<DailyPaper>>() {}); |
||||
|
|
||||
|
weekPlanResponse.setDailyPapers(dailyPaperList); |
||||
|
|
||||
|
// 把结果存入 state |
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.mcp; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.Dept; |
||||
|
import com.chenhai.chenhaiai.entity.UserInfo; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
import org.springframework.ai.tool.ToolCallback; |
||||
|
import org.springframework.ai.tool.ToolCallbackProvider; |
||||
|
import org.springframework.core.ParameterizedTypeReference; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class DeptNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public DeptNode(ChatClient chatClient) { |
||||
|
this.chatClient = chatClient; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 输入数据 |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
String deptName = weekPlanResponse.getDeptName(); |
||||
|
|
||||
|
// 提示词 |
||||
|
String prompt = """ |
||||
|
请使用可用的工具查询sys_dept表dept_name=%s的唯一数据,只保留字段dept_id,dept_name |
||||
|
""".formatted(deptName); |
||||
|
|
||||
|
// 模型调用 |
||||
|
Dept dept = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.entity(Dept.class); |
||||
|
|
||||
|
weekPlanResponse.setDept(dept); |
||||
|
|
||||
|
// 把结果存入 state |
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.mcp; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.UserInfo; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
import org.springframework.ai.tool.ToolCallback; |
||||
|
import org.springframework.ai.tool.ToolCallbackProvider; |
||||
|
import org.springframework.core.ParameterizedTypeReference; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class UserNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public UserNode(ChatClient chatClient) { |
||||
|
this.chatClient = chatClient; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 输入数据 |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
Long deptId = weekPlanResponse.getDept().getDeptId(); |
||||
|
|
||||
|
// 提示词 |
||||
|
String prompt = """ |
||||
|
请使用可用的工具查询sys_user表dept_id=%s的列表数据,只保留字段user_id,user_name |
||||
|
""".formatted(deptId); |
||||
|
|
||||
|
// 模型调用 |
||||
|
List<UserInfo> userInfos = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.entity(new ParameterizedTypeReference<List<UserInfo>>() {}); |
||||
|
|
||||
|
weekPlanResponse.setUserInfos(userInfos); |
||||
|
|
||||
|
// 把结果存入 state |
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.mcp; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanDetail; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
import org.springframework.core.ParameterizedTypeReference; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class WeekPlanDetailNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public WeekPlanDetailNode(ChatClient chatClient) { |
||||
|
this.chatClient = chatClient; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 输入数据 |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
Long mainId = weekPlanResponse.getPlanMain().getId(); |
||||
|
|
||||
|
// 提示词 |
||||
|
String prompt = """ |
||||
|
请使用可用的工具查询ch_week_project表main_id=%d的所有数据, |
||||
|
返回字段:project_name, content, developer, supervise_status, note, project_note |
||||
|
请完整返回查询结果 |
||||
|
""".formatted(mainId); |
||||
|
|
||||
|
// 模型调用 |
||||
|
List<WeekPlanDetail> weekPlanDetailList = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.entity(new ParameterizedTypeReference<List<WeekPlanDetail>>() {}); |
||||
|
|
||||
|
weekPlanResponse.setPlanDetails(weekPlanDetailList); |
||||
|
|
||||
|
// 把结果存入 state |
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
package com.chenhai.chenhaiai.node.weekPlan.mcp; |
||||
|
|
||||
|
import com.alibaba.cloud.ai.graph.OverAllState; |
||||
|
import com.alibaba.cloud.ai.graph.action.NodeAction; |
||||
|
import com.chenhai.chenhaiai.entity.UserInfo; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanMain; |
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
||||
|
import org.springframework.core.ParameterizedTypeReference; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* @author : mazhongxu |
||||
|
* @date : 2025-12-04 19:06 |
||||
|
* @modyified By : |
||||
|
*/ |
||||
|
public class WeekPlanMainNode implements NodeAction { |
||||
|
private final ChatClient chatClient; |
||||
|
|
||||
|
public WeekPlanMainNode(ChatClient chatClient) { |
||||
|
this.chatClient = chatClient; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, Object> apply(OverAllState state) throws Exception { |
||||
|
// 从state 中获取 输入数据 |
||||
|
WeekPlanResponse weekPlanResponse = state.value("weekPlanResponse", new WeekPlanResponse()); |
||||
|
String deptName = weekPlanResponse.getDeptName(); |
||||
|
String weekDisplay = weekPlanResponse.getWeekDisplay(); |
||||
|
|
||||
|
// 提示词 |
||||
|
String prompt = """ |
||||
|
请使用可用的工具查询ch_week_plan表dept_name='%s' AND week_display='%s'的唯一数据, |
||||
|
只返回字段:id, dept_name, week_display, week_start_date, week_end_date |
||||
|
""".formatted(deptName, weekDisplay); |
||||
|
|
||||
|
// 模型调用 |
||||
|
WeekPlanMain weekPlanMain = chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.call() |
||||
|
.entity(WeekPlanMain.class); |
||||
|
|
||||
|
weekPlanResponse.setPlanMain(weekPlanMain); |
||||
|
|
||||
|
// 把结果存入 state |
||||
|
return Map.of("weekPlanResponse", weekPlanResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
// AnalysisStreamService.java |
||||
|
package com.chenhai.chenhaiai.service; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.WeekPlanResponse; |
||||
|
import com.chenhai.chenhaiai.utils.TextFormatUtils; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import reactor.core.publisher.FluxSink; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
|
||||
|
/** |
||||
|
* 流式分析服务 |
||||
|
*/ |
||||
|
@Service |
||||
|
public class AnalysisStreamService { |
||||
|
|
||||
|
private final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
/** |
||||
|
* 发送流式分析结果 |
||||
|
*/ |
||||
|
public void sendStreamAnalysis( |
||||
|
FluxSink<String> sink, |
||||
|
String analysisContent, |
||||
|
ChatClient chatClient, |
||||
|
String prompt) { |
||||
|
|
||||
|
try { |
||||
|
// 使用 StringBuilder 收集完整响应 |
||||
|
StringBuilder fullResponse = new StringBuilder(); |
||||
|
|
||||
|
chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.stream() |
||||
|
.content() |
||||
|
.subscribe( |
||||
|
chunk -> { |
||||
|
// 收集所有chunk |
||||
|
fullResponse.append(chunk); |
||||
|
}, |
||||
|
error -> { |
||||
|
sink.next(TextFormatUtils.formatMessage("error", |
||||
|
"分析失败: " + error.getMessage())); |
||||
|
sink.complete(); |
||||
|
}, |
||||
|
() -> { |
||||
|
try { |
||||
|
// 在流式响应完成后,按模块分割发送 |
||||
|
String completeResponse = fullResponse.toString(); |
||||
|
sendFormattedModules(sink, completeResponse); |
||||
|
} catch (Exception e) { |
||||
|
sink.next(TextFormatUtils.formatMessage("error", |
||||
|
"格式化输出失败: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
sink.next(TextFormatUtils.formatMessage("error", "流式分析失败: " + e.getMessage())); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送格式化模块 |
||||
|
*/ |
||||
|
private void sendFormattedModules(FluxSink<String> sink, String analysisContent) { |
||||
|
// 使用工具类分割模块 |
||||
|
String[] modules = TextFormatUtils.splitAnalysisModules(analysisContent); |
||||
|
|
||||
|
for (String module : modules) { |
||||
|
String trimmedModule = module.trim(); |
||||
|
if (!trimmedModule.isEmpty()) { |
||||
|
// 发送完整的模块 |
||||
|
sink.next(TextFormatUtils.formatMessage("content", trimmedModule)); |
||||
|
|
||||
|
// 每个模块之间稍微延迟,让前端有时间渲染 |
||||
|
try { |
||||
|
Thread.sleep(200); |
||||
|
} catch (InterruptedException e) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 发送完成消息 |
||||
|
sink.next(TextFormatUtils.formatMessage("complete", "分析完成")); |
||||
|
sink.complete(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 准备分析提示词 |
||||
|
*/ |
||||
|
public String prepareAnalysisPrompt(String promptTemplate, WeekPlanResponse fullData) { |
||||
|
try { |
||||
|
String jsonData = objectMapper.writeValueAsString(fullData); |
||||
|
return promptTemplate.replace("{jsonData}", jsonData); |
||||
|
} catch (Exception e) { |
||||
|
throw new RuntimeException("准备分析提示词失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取数据概览消息 |
||||
|
*/ |
||||
|
public String getDataSummary(WeekPlanResponse fullData) { |
||||
|
int planCount = fullData.getPlanDetails() != null ? fullData.getPlanDetails().size() : 0; |
||||
|
int dailyCount = fullData.getDailyPapers() != null ? fullData.getDailyPapers().size() : 0; |
||||
|
|
||||
|
return String.format("获取到 %d 个计划任务和 %d 条日报记录", planCount, dailyCount); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,583 @@ |
|||||
|
package com.chenhai.chenhaiai.service; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.GiteaCommit; |
||||
|
import com.chenhai.chenhaiai.entity.git.GiteaRepository; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
|
||||
|
import java.net.http.HttpClient; |
||||
|
import java.net.http.HttpRequest; |
||||
|
import java.net.http.HttpResponse; |
||||
|
import java.time.*; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.*; |
||||
|
import java.util.concurrent.*; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class GiteaAnalysisParallelService implements AutoCloseable { |
||||
|
private final String giteaBaseUrl; |
||||
|
private final String accessToken; |
||||
|
private final ObjectMapper objectMapper; |
||||
|
private final HttpClient httpClient; |
||||
|
|
||||
|
// 线程池 |
||||
|
private final ExecutorService executorService; |
||||
|
|
||||
|
// 缓存 |
||||
|
private final Map<String, List<GiteaRepository>> userReposCache = new ConcurrentHashMap<>(); |
||||
|
private final Map<String, List<GiteaCommit>> repoCommitsCache = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
// 关闭标志 |
||||
|
private volatile boolean isShutdown = false; |
||||
|
|
||||
|
public GiteaAnalysisParallelService(String giteaBaseUrl, String accessToken) { |
||||
|
this.giteaBaseUrl = giteaBaseUrl; |
||||
|
this.accessToken = accessToken; |
||||
|
this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); |
||||
|
|
||||
|
// 创建HTTP客户端 |
||||
|
this.httpClient = HttpClient.newBuilder() |
||||
|
.connectTimeout(Duration.ofSeconds(5)) |
||||
|
.build(); |
||||
|
|
||||
|
// 创建分析用的线程池 |
||||
|
int coreCount = Runtime.getRuntime().availableProcessors(); |
||||
|
this.executorService = new ThreadPoolExecutor( |
||||
|
coreCount, |
||||
|
coreCount * 2, |
||||
|
60L, TimeUnit.SECONDS, |
||||
|
new LinkedBlockingQueue<>(100), |
||||
|
new ThreadPoolExecutor.CallerRunsPolicy() |
||||
|
); |
||||
|
|
||||
|
// 添加JVM关闭钩子,确保资源释放 |
||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> { |
||||
|
try { |
||||
|
if (!isShutdown) { |
||||
|
shutdown(); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.error("Shutdown hook error", e); |
||||
|
} |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
// ==================== 唯一公开方法 ==================== |
||||
|
|
||||
|
public String performCompleteAnalysis(String since, String until) { |
||||
|
if (isShutdown) { |
||||
|
throw new IllegalStateException("服务已关闭"); |
||||
|
} |
||||
|
|
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
StringBuilder report = new StringBuilder(); |
||||
|
|
||||
|
System.out.println("=".repeat(60)); |
||||
|
System.out.println("🚀 Gitea代码仓库分析(极速版)"); |
||||
|
System.out.println("📅 时间范围: " + since + " 至 " + until); |
||||
|
System.out.println("=".repeat(60)); |
||||
|
|
||||
|
try { |
||||
|
// 1. 获取所有仓库 |
||||
|
System.out.println("\n📦 步骤1: 获取仓库列表..."); |
||||
|
List<GiteaRepository> allRepos = getAllUserRepositories(); |
||||
|
int totalRepos = allRepos.size(); |
||||
|
System.out.println(" 发现仓库: " + totalRepos + " 个"); |
||||
|
|
||||
|
if (totalRepos == 0) { |
||||
|
return "❌ 未发现任何仓库"; |
||||
|
} |
||||
|
|
||||
|
// 2. 解析时间范围 |
||||
|
ZonedDateTime sinceTime = ZonedDateTime.parse(since); |
||||
|
ZonedDateTime untilTime = ZonedDateTime.parse(until); |
||||
|
|
||||
|
// 3. 并行分析所有仓库 |
||||
|
System.out.println("\n⚡ 步骤2: 并行分析仓库提交..."); |
||||
|
|
||||
|
// 创建共享的数据容器 |
||||
|
Map<String, DeveloperData> devDataMap = new ConcurrentHashMap<>(); |
||||
|
Map<String, RepoData> repoDataMap = new ConcurrentHashMap<>(); |
||||
|
Map<DayOfWeek, Integer> dayStats = new ConcurrentHashMap<>(); |
||||
|
Map<Integer, Integer> hourStats = new ConcurrentHashMap<>(); |
||||
|
Map<String, Integer> fileTypeStats = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
AtomicInteger activeRepos = new AtomicInteger(0); |
||||
|
AtomicInteger totalCommits = new AtomicInteger(0); |
||||
|
AtomicInteger processed = new AtomicInteger(0); |
||||
|
|
||||
|
// 使用CountDownLatch等待所有任务完成 |
||||
|
CountDownLatch latch = new CountDownLatch(totalRepos); |
||||
|
|
||||
|
// 提交所有仓库分析任务 |
||||
|
for (GiteaRepository repo : allRepos) { |
||||
|
executorService.submit(() -> { |
||||
|
try { |
||||
|
if (isShutdown) { |
||||
|
return; |
||||
|
} |
||||
|
analyzeRepository(repo, sinceTime, untilTime, |
||||
|
devDataMap, repoDataMap, dayStats, hourStats, fileTypeStats, |
||||
|
activeRepos, totalCommits); |
||||
|
} catch (Exception e) { |
||||
|
log.debug("仓库 {} 分析失败: {}", repo.getFullPath(), e.getMessage()); |
||||
|
} finally { |
||||
|
// 更新进度 |
||||
|
int done = processed.incrementAndGet(); |
||||
|
if (done % 5 == 0 || done == totalRepos) { |
||||
|
System.out.printf(" 进度: %d/%d | 活跃: %d | 提交: %d\r", |
||||
|
done, totalRepos, activeRepos.get(), totalCommits.get()); |
||||
|
} |
||||
|
latch.countDown(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 等待所有任务完成,最多30秒 |
||||
|
boolean completed = latch.await(30, TimeUnit.SECONDS); |
||||
|
if (!completed) { |
||||
|
System.out.println("\n⚠️ 部分仓库分析超时,继续处理已完成结果..."); |
||||
|
} |
||||
|
|
||||
|
System.out.println(); // 换行 |
||||
|
|
||||
|
// 4. 生成报告 |
||||
|
System.out.println("\n📊 步骤3: 生成分析报告..."); |
||||
|
|
||||
|
long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
report.append(generateReport(since, until, totalRepos, activeRepos.get(), |
||||
|
devDataMap.size(), totalCommits.get(), devDataMap, repoDataMap, |
||||
|
dayStats, hourStats, fileTypeStats, analysisTime)); |
||||
|
|
||||
|
System.out.println("\n📤 分析完成"); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.println("\n❌ 分析失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
report.append("分析失败: ").append(e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
return report.toString(); |
||||
|
} |
||||
|
|
||||
|
// ==================== 核心分析方法 ==================== |
||||
|
|
||||
|
/** |
||||
|
* 分析单个仓库(并行执行) |
||||
|
*/ |
||||
|
private void analyzeRepository(GiteaRepository repo, |
||||
|
ZonedDateTime sinceTime, |
||||
|
ZonedDateTime untilTime, |
||||
|
Map<String, DeveloperData> devDataMap, |
||||
|
Map<String, RepoData> repoDataMap, |
||||
|
Map<DayOfWeek, Integer> dayStats, |
||||
|
Map<Integer, Integer> hourStats, |
||||
|
Map<String, Integer> fileTypeStats, |
||||
|
AtomicInteger activeRepos, |
||||
|
AtomicInteger totalCommits) throws Exception { |
||||
|
|
||||
|
String repoFullName = repo.getFullPath(); |
||||
|
|
||||
|
// 获取此仓库的提交 |
||||
|
List<GiteaCommit> commits = getRepoCommits(repoFullName); |
||||
|
|
||||
|
// 按时间范围过滤 |
||||
|
List<GiteaCommit> commitsInRange = new ArrayList<>(); |
||||
|
for (GiteaCommit commit : commits) { |
||||
|
if (commit.isWithinTimeRange(sinceTime, untilTime)) { |
||||
|
commitsInRange.add(commit); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!commitsInRange.isEmpty()) { |
||||
|
activeRepos.incrementAndGet(); |
||||
|
|
||||
|
// 创建仓库数据 |
||||
|
RepoData repoData = new RepoData(); |
||||
|
repoData.repoName = repoFullName; |
||||
|
repoData.displayName = repo.getRepoName(); |
||||
|
|
||||
|
// 分析每个提交 |
||||
|
for (GiteaCommit commit : commitsInRange) { |
||||
|
totalCommits.incrementAndGet(); |
||||
|
|
||||
|
// 获取作者 |
||||
|
String author = getAuthorName(commit); |
||||
|
|
||||
|
// 更新开发者数据 |
||||
|
DeveloperData devData = devDataMap.computeIfAbsent(author, |
||||
|
k -> new DeveloperData(author)); |
||||
|
devData.commitCount++; |
||||
|
devData.repos.add(repoFullName); |
||||
|
|
||||
|
// 更新仓库数据 |
||||
|
repoData.commitCount++; |
||||
|
repoData.developers.add(author); |
||||
|
|
||||
|
// 时间统计 |
||||
|
ZonedDateTime commitTime = commit.getCommitTime(); |
||||
|
if (commitTime != null) { |
||||
|
DayOfWeek day = commitTime.getDayOfWeek(); |
||||
|
int hour = commitTime.getHour(); |
||||
|
|
||||
|
dayStats.merge(day, 1, Integer::sum); |
||||
|
hourStats.merge(hour, 1, Integer::sum); |
||||
|
} |
||||
|
|
||||
|
// 文件类型统计 |
||||
|
if (commit.getFiles() != null) { |
||||
|
for (GiteaCommit.ChangedFile file : commit.getFiles()) { |
||||
|
String fileType = file.getFileType(); |
||||
|
fileTypeStats.merge(fileType, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 保存仓库数据 |
||||
|
repoDataMap.put(repoFullName, repoData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取仓库的所有提交(带缓存,只取最近3个月) |
||||
|
*/ |
||||
|
private List<GiteaCommit> getRepoCommits(String repoFullName) throws Exception { |
||||
|
// 检查缓存 |
||||
|
if (repoCommitsCache.containsKey(repoFullName)) { |
||||
|
return repoCommitsCache.get(repoFullName); |
||||
|
} |
||||
|
|
||||
|
// 只获取最近3个月的提交 |
||||
|
LocalDate threeMonthsAgo = LocalDate.now().minusMonths(3); |
||||
|
ZonedDateTime since = threeMonthsAgo.atStartOfDay(ZoneId.systemDefault()); |
||||
|
ZonedDateTime until = ZonedDateTime.now(); |
||||
|
|
||||
|
String sinceStr = since.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
String untilStr = until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
|
||||
|
String url = String.format("%s/api/v1/repos/%s/commits?since=%s&until=%s", |
||||
|
giteaBaseUrl, repoFullName, sinceStr, untilStr); |
||||
|
|
||||
|
List<GiteaCommit> commits = fetchCommits(url); |
||||
|
repoCommitsCache.put(repoFullName, commits); |
||||
|
|
||||
|
return commits; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 分页获取提交列表 |
||||
|
*/ |
||||
|
private List<GiteaCommit> fetchCommits(String url) throws Exception { |
||||
|
if (isShutdown) { |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
|
||||
|
List<GiteaCommit> results = new ArrayList<>(); |
||||
|
int page = 1; |
||||
|
|
||||
|
while (true) { |
||||
|
String pageUrl = url + (url.contains("?") ? "&" : "?") + |
||||
|
"page=" + page + "&limit=50"; |
||||
|
|
||||
|
HttpRequest request = HttpRequest.newBuilder() |
||||
|
.uri(java.net.URI.create(pageUrl)) |
||||
|
.header("Authorization", "token " + accessToken) |
||||
|
.header("Accept", "application/json") |
||||
|
.timeout(Duration.ofSeconds(10)) |
||||
|
.GET() |
||||
|
.build(); |
||||
|
|
||||
|
HttpResponse<String> response = httpClient.send(request, |
||||
|
HttpResponse.BodyHandlers.ofString()); |
||||
|
|
||||
|
if (response.statusCode() != 200) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
List<GiteaCommit> pageResults = objectMapper.readValue( |
||||
|
response.body(), |
||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, GiteaCommit.class)); |
||||
|
|
||||
|
if (pageResults.isEmpty()) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
results.addAll(pageResults); |
||||
|
|
||||
|
// 如果数量少于限制,说明是最后一页 |
||||
|
if (pageResults.size() < 50) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
page++; |
||||
|
} |
||||
|
|
||||
|
return results; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取用户所有仓库 |
||||
|
*/ |
||||
|
public List<GiteaRepository> getAllUserRepositories() throws Exception { |
||||
|
if (isShutdown) { |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
|
||||
|
String cacheKey = "user_repos"; |
||||
|
if (userReposCache.containsKey(cacheKey)) { |
||||
|
return userReposCache.get(cacheKey); |
||||
|
} |
||||
|
|
||||
|
String url = giteaBaseUrl + "/api/v1/user/repos?limit=100"; |
||||
|
List<GiteaRepository> repos = fetchRepositories(url); |
||||
|
userReposCache.put(cacheKey, repos); |
||||
|
|
||||
|
return repos; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 分页获取仓库列表 |
||||
|
*/ |
||||
|
private List<GiteaRepository> fetchRepositories(String url) throws Exception { |
||||
|
if (isShutdown) { |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
|
||||
|
List<GiteaRepository> results = new ArrayList<>(); |
||||
|
int page = 1; |
||||
|
|
||||
|
while (true) { |
||||
|
String pageUrl = url + (url.contains("?") ? "&" : "?") + |
||||
|
"page=" + page + "&limit=50"; |
||||
|
|
||||
|
HttpRequest request = HttpRequest.newBuilder() |
||||
|
.uri(java.net.URI.create(pageUrl)) |
||||
|
.header("Authorization", "token " + accessToken) |
||||
|
.header("Accept", "application/json") |
||||
|
.timeout(Duration.ofSeconds(10)) |
||||
|
.GET() |
||||
|
.build(); |
||||
|
|
||||
|
HttpResponse<String> response = httpClient.send(request, |
||||
|
HttpResponse.BodyHandlers.ofString()); |
||||
|
|
||||
|
if (response.statusCode() != 200) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
List<GiteaRepository> pageResults = objectMapper.readValue( |
||||
|
response.body(), |
||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, GiteaRepository.class)); |
||||
|
|
||||
|
if (pageResults.isEmpty()) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
results.addAll(pageResults); |
||||
|
|
||||
|
// 如果数量少于限制,说明是最后一页 |
||||
|
if (pageResults.size() < 50) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
page++; |
||||
|
} |
||||
|
|
||||
|
return results; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取提交作者名称 |
||||
|
*/ |
||||
|
private String getAuthorName(GiteaCommit commit) { |
||||
|
if (commit.getCommit() != null && |
||||
|
commit.getCommit().getAuthor() != null && |
||||
|
commit.getCommit().getAuthor().getName() != null) { |
||||
|
return commit.getCommit().getAuthor().getName(); |
||||
|
} |
||||
|
return "未知作者"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成分析报告 |
||||
|
*/ |
||||
|
private String generateReport(String since, String until, |
||||
|
int totalRepos, int activeRepos, int totalDevs, int totalCommits, |
||||
|
Map<String, DeveloperData> devDataMap, |
||||
|
Map<String, RepoData> repoDataMap, |
||||
|
Map<DayOfWeek, Integer> dayStats, |
||||
|
Map<Integer, Integer> hourStats, |
||||
|
Map<String, Integer> fileTypeStats, |
||||
|
long analysisTime) { |
||||
|
|
||||
|
StringBuilder report = new StringBuilder(); |
||||
|
report.append("=".repeat(80)).append("\n"); |
||||
|
report.append("📈 GITEA 开发活动分析报告\n"); |
||||
|
report.append("=".repeat(80)).append("\n\n"); |
||||
|
|
||||
|
// 基础统计 |
||||
|
report.append("📅 时间范围: ").append(since).append(" 至 ").append(until).append("\n"); |
||||
|
report.append("🏢 仓库总数: ").append(totalRepos).append(" 个\n"); |
||||
|
report.append("🎯 活跃仓库: ").append(activeRepos).append(" 个\n"); |
||||
|
report.append("👥 活跃开发者: ").append(totalDevs).append(" 人\n"); |
||||
|
report.append("💾 提交总数: ").append(totalCommits).append(" 次\n"); |
||||
|
report.append("⏱️ 分析耗时: ").append(analysisTime).append("ms\n\n"); |
||||
|
|
||||
|
// 开发者排行榜 |
||||
|
report.append("🏆 开发者排行榜(TOP 10)\n"); |
||||
|
report.append("-".repeat(60)).append("\n"); |
||||
|
|
||||
|
List<DeveloperData> devList = new ArrayList<>(devDataMap.values()); |
||||
|
devList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
|
||||
|
int rank = 1; |
||||
|
for (DeveloperData dev : devList) { |
||||
|
if (rank > 10) break; |
||||
|
report.append(String.format(" %2d. %-20s %4d 次提交 | %2d 个仓库%n", |
||||
|
rank++, dev.name, dev.commitCount, dev.repos.size())); |
||||
|
} |
||||
|
|
||||
|
// 仓库排行榜 |
||||
|
report.append("\n🏢 活跃仓库排行榜(TOP 10)\n"); |
||||
|
report.append("-".repeat(60)).append("\n"); |
||||
|
|
||||
|
List<RepoData> repoList = new ArrayList<>(repoDataMap.values()); |
||||
|
repoList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
|
||||
|
rank = 1; |
||||
|
for (RepoData repo : repoList) { |
||||
|
if (rank > 10) break; |
||||
|
report.append(String.format(" %2d. %-40s %4d 次提交 | %2d 个开发者%n", |
||||
|
rank++, truncate(repo.repoName, 40), repo.commitCount, repo.developers.size())); |
||||
|
} |
||||
|
|
||||
|
// 时间分布 |
||||
|
report.append("\n⏰ 提交时间分布\n"); |
||||
|
report.append("-".repeat(40)).append("\n"); |
||||
|
|
||||
|
String[] dayNames = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; |
||||
|
DayOfWeek[] days = DayOfWeek.values(); |
||||
|
|
||||
|
for (int i = 0; i < 7; i++) { |
||||
|
int count = dayStats.getOrDefault(days[i], 0); |
||||
|
report.append(String.format(" %-4s: %3d 次%n", dayNames[i], count)); |
||||
|
} |
||||
|
|
||||
|
report.append("\n按小时分布:\n"); |
||||
|
for (int hour = 0; hour < 24; hour++) { |
||||
|
int count = hourStats.getOrDefault(hour, 0); |
||||
|
if (count > 0) { |
||||
|
report.append(String.format(" %02d:00-%02d:59: %3d 次%n", hour, hour, count)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 文件类型 |
||||
|
report.append("\n📁 修改的文件类型\n"); |
||||
|
report.append("-".repeat(40)).append("\n"); |
||||
|
|
||||
|
List<Map.Entry<String, Integer>> fileList = new ArrayList<>(fileTypeStats.entrySet()); |
||||
|
fileList.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); |
||||
|
|
||||
|
for (Map.Entry<String, Integer> entry : fileList) { |
||||
|
report.append(String.format(" %-10s: %4d 个文件%n", entry.getKey(), entry.getValue())); |
||||
|
} |
||||
|
|
||||
|
report.append("\n").append("=".repeat(80)).append("\n"); |
||||
|
report.append("生成时间: ").append(LocalDateTime.now() |
||||
|
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("\n"); |
||||
|
report.append("=".repeat(80)); |
||||
|
|
||||
|
return report.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 字符串截断 |
||||
|
*/ |
||||
|
private String truncate(String str, int length) { |
||||
|
if (str == null || str.length() <= length) return str; |
||||
|
return str.substring(0, length - 3) + "..."; |
||||
|
} |
||||
|
|
||||
|
// ==================== 内部数据类 ==================== |
||||
|
|
||||
|
private static class DeveloperData { |
||||
|
String name; |
||||
|
int commitCount = 0; |
||||
|
Set<String> repos = new HashSet<>(); |
||||
|
|
||||
|
DeveloperData(String name) { |
||||
|
this.name = name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static class RepoData { |
||||
|
String repoName; |
||||
|
String displayName; |
||||
|
int commitCount = 0; |
||||
|
Set<String> developers = new HashSet<>(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 关闭服务,释放资源 |
||||
|
*/ |
||||
|
public void shutdown() { |
||||
|
if (isShutdown) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
log.info("正在关闭Gitea分析服务..."); |
||||
|
isShutdown = true; |
||||
|
|
||||
|
// 1. 先停止接收新任务 |
||||
|
executorService.shutdown(); |
||||
|
|
||||
|
try { |
||||
|
// 2. 等待现有任务完成(最多30秒) |
||||
|
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { |
||||
|
// 3. 如果超时,强制关闭 |
||||
|
log.warn("线程池未在30秒内关闭,尝试强制关闭..."); |
||||
|
executorService.shutdownNow(); |
||||
|
|
||||
|
// 再等待一段时间 |
||||
|
if (!executorService.awaitTermination(15, TimeUnit.SECONDS)) { |
||||
|
log.error("线程池无法关闭"); |
||||
|
} |
||||
|
} |
||||
|
} catch (InterruptedException e) { |
||||
|
executorService.shutdownNow(); |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} |
||||
|
|
||||
|
// 4. 清理缓存 |
||||
|
userReposCache.clear(); |
||||
|
repoCommitsCache.clear(); |
||||
|
|
||||
|
log.info("Gitea分析服务已关闭"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 实现AutoCloseable接口,支持try-with-resources |
||||
|
*/ |
||||
|
@Override |
||||
|
public void close() { |
||||
|
shutdown(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加缓存清理方法(可选) |
||||
|
*/ |
||||
|
public void clearCache() { |
||||
|
userReposCache.clear(); |
||||
|
repoCommitsCache.clear(); |
||||
|
log.info("缓存已清理"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查服务是否已关闭 |
||||
|
*/ |
||||
|
public boolean isShutdown() { |
||||
|
return isShutdown; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
package com.chenhai.chenhaiai.service; |
||||
|
|
||||
|
import java.time.*; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.time.temporal.TemporalAdjusters; |
||||
|
|
||||
|
/** |
||||
|
* Gitea分析测试类 - 极速版 |
||||
|
*/ |
||||
|
public class GiteaAnalysisParallelTest { |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
System.out.println("🚀 开始Gitea代码仓库分析..."); |
||||
|
long totalStartTime = System.currentTimeMillis(); |
||||
|
|
||||
|
// 使用try-with-resources确保资源正确关闭 |
||||
|
try (GiteaAnalysisParallelService service = new GiteaAnalysisParallelService( |
||||
|
"http://192.168.1.224:3000", |
||||
|
"a9f1c8d3d6fefd73956604f496457faaa3672f89" |
||||
|
)) { |
||||
|
|
||||
|
// 1. 生成上周时间范围(上周一~上周日) |
||||
|
LocalDate lastMonday = LocalDate.now().minusWeeks(1) |
||||
|
.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); |
||||
|
String since = lastMonday.atStartOfDay(ZoneId.systemDefault()) |
||||
|
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
String until = lastMonday.plusDays(6).atTime(23, 59, 59) |
||||
|
.atZone(ZoneId.systemDefault()) |
||||
|
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
|
||||
|
System.out.println("📅 分析时间: " + lastMonday + " 至 " + lastMonday.plusDays(6)); |
||||
|
|
||||
|
// 2. 执行分析并获取报告 |
||||
|
long analysisStartTime = System.currentTimeMillis(); |
||||
|
String report = service.performCompleteAnalysis(since, until); |
||||
|
long analysisEndTime = System.currentTimeMillis(); |
||||
|
|
||||
|
// 3. 打印报告 |
||||
|
System.out.println(report); |
||||
|
System.out.println("⏱️ 分析耗时: " + (analysisEndTime - analysisStartTime) + "ms"); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.println("❌ 分析失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} finally { |
||||
|
long totalEndTime = System.currentTimeMillis(); |
||||
|
System.out.println("✅ 测试完成,总耗时: " + (totalEndTime - totalStartTime) + "ms"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,570 @@ |
|||||
|
package com.chenhai.chenhaiai.service; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.*; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.core.task.TaskExecutor; |
||||
|
import org.springframework.scheduling.annotation.Async; |
||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.net.http.HttpClient; |
||||
|
import java.net.http.HttpRequest; |
||||
|
import java.net.http.HttpResponse; |
||||
|
import java.time.DayOfWeek; |
||||
|
import java.time.Duration; |
||||
|
import java.time.LocalDateTime; |
||||
|
import java.time.ZonedDateTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.*; |
||||
|
import java.util.concurrent.CompletableFuture; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class GiteaAnalysisService { |
||||
|
|
||||
|
@Value("${gitea.url:http://192.168.1.224:3000}") |
||||
|
private String giteaBaseUrl; |
||||
|
|
||||
|
@Value("${gitea.token:a9f1c8d3d6fefd73956604f496457faaa3672f89}") |
||||
|
private String accessToken; |
||||
|
|
||||
|
@Value("${gitea.analysis.max-active-repos:100}") |
||||
|
private int maxActiveRepos; |
||||
|
|
||||
|
@Autowired |
||||
|
private ThreadPoolTaskExecutor giteaTaskExecutor; |
||||
|
|
||||
|
@Autowired |
||||
|
private TaskExecutor taskExecutor; |
||||
|
|
||||
|
private HttpClient httpClient; |
||||
|
private ObjectMapper objectMapper; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void init() { |
||||
|
log.info("初始化Gitea分析服务..."); |
||||
|
|
||||
|
httpClient = HttpClient.newBuilder() |
||||
|
.connectTimeout(Duration.ofSeconds(10)) |
||||
|
.build(); |
||||
|
|
||||
|
objectMapper = new ObjectMapper(); |
||||
|
objectMapper.registerModule(new JavaTimeModule()); |
||||
|
|
||||
|
log.info("Gitea分析服务初始化完成"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 异步执行Git分析,直接返回结构化数据 |
||||
|
* @param since 开始时间 (ISO格式: 2025-12-01T00:00:00+08:00) |
||||
|
* @param until 结束时间 (ISO格式: 2025-12-31T23:59:59+08:00) |
||||
|
* @return 结构化分析数据 |
||||
|
*/ |
||||
|
@Async("giteaTaskExecutor") |
||||
|
public CompletableFuture<GitAnalysisData> analyzeGitDataAsync(String since, String until) { |
||||
|
String taskId = UUID.randomUUID().toString().substring(0, 8); |
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
|
||||
|
log.info("开始Git分析任务[{}]: {} 至 {}", taskId, since, until); |
||||
|
|
||||
|
return CompletableFuture.supplyAsync(() -> { |
||||
|
try { |
||||
|
GitAnalysisData analysisData = performCompleteAnalysis(since, until, taskId); |
||||
|
long cost = System.currentTimeMillis() - startTime; |
||||
|
log.info("Git分析任务[{}]完成,耗时: {}ms", taskId, cost); |
||||
|
return analysisData; |
||||
|
} catch (Exception e) { |
||||
|
log.error("Git分析任务[{}]失败: {}", taskId, e.getMessage(), e); |
||||
|
throw new RuntimeException("Git分析失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
}, giteaTaskExecutor); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 执行完整分析,返回结构化数据 |
||||
|
*/ |
||||
|
private GitAnalysisData performCompleteAnalysis(String since, String until, String taskId) throws Exception { |
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
|
||||
|
// 1. 获取所有仓库 |
||||
|
log.debug("任务[{}] 获取仓库列表...", taskId); |
||||
|
List<GiteaRepository> allRepos = getAllUserRepositories(); |
||||
|
int totalRepos = allRepos.size(); |
||||
|
|
||||
|
if (totalRepos == 0) { |
||||
|
// 返回空的基础数据 |
||||
|
return buildEmptyGitAnalysisData("无仓库数据"); |
||||
|
} |
||||
|
|
||||
|
log.info("任务[{}] 发现仓库: {} 个", taskId, totalRepos); |
||||
|
|
||||
|
// 2. 解析时间范围 |
||||
|
ZonedDateTime sinceTime = ZonedDateTime.parse(since); |
||||
|
ZonedDateTime untilTime = ZonedDateTime.parse(until); |
||||
|
|
||||
|
// 3. 快速筛选活跃仓库 |
||||
|
log.debug("任务[{}] 快速筛选活跃仓库...", taskId); |
||||
|
List<GiteaRepository> activeRepos = findActiveRepositories(allRepos, sinceTime, untilTime, taskId) |
||||
|
.get(20, TimeUnit.SECONDS); |
||||
|
int activeRepoCount = activeRepos.size(); |
||||
|
|
||||
|
log.info("任务[{}] 活跃仓库: {} 个", taskId, activeRepoCount); |
||||
|
|
||||
|
if (activeRepoCount == 0) { |
||||
|
// 返回简单结果(无活跃仓库) |
||||
|
return buildSimpleGitAnalysisData(since, until, totalRepos, 0, 0, startTime); |
||||
|
} |
||||
|
|
||||
|
// 限制活跃仓库数量 |
||||
|
if (activeRepoCount > maxActiveRepos) { |
||||
|
log.warn("任务[{}] 活跃仓库过多({}个),采样分析前{}个", taskId, activeRepoCount, maxActiveRepos); |
||||
|
activeRepos = activeRepos.subList(0, maxActiveRepos); |
||||
|
activeRepoCount = maxActiveRepos; |
||||
|
} |
||||
|
|
||||
|
// 4. 详细分析活跃仓库 |
||||
|
log.debug("任务[{}] 详细分析活跃仓库...", taskId); |
||||
|
DetailedAnalysisResult detailResult = analyzeActiveRepositories(activeRepos, sinceTime, untilTime, taskId) |
||||
|
.get(20, TimeUnit.SECONDS); |
||||
|
|
||||
|
// 5. 构建结构化数据 |
||||
|
long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
return buildGitAnalysisData(since, until, totalRepos, activeRepoCount, detailResult, analysisTime); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 快速筛选活跃仓库 |
||||
|
*/ |
||||
|
private CompletableFuture<List<GiteaRepository>> findActiveRepositories(List<GiteaRepository> allRepos, |
||||
|
ZonedDateTime since, |
||||
|
ZonedDateTime until, |
||||
|
String taskId) { |
||||
|
return CompletableFuture.supplyAsync(() -> { |
||||
|
List<GiteaRepository> activeRepos = Collections.synchronizedList(new ArrayList<>()); |
||||
|
List<CompletableFuture<Boolean>> futures = new ArrayList<>(); |
||||
|
AtomicInteger checked = new AtomicInteger(0); |
||||
|
final int totalRepos = allRepos.size(); |
||||
|
|
||||
|
for (GiteaRepository repo : allRepos) { |
||||
|
final GiteaRepository currentRepo = repo; |
||||
|
|
||||
|
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> { |
||||
|
try { |
||||
|
return hasCommitsInRange(currentRepo.getFullPath(), since, until); |
||||
|
} catch (Exception e) { |
||||
|
log.debug("任务[{}] 仓库 {} 快速检查失败: {}", taskId, currentRepo.getFullPath(), e.getMessage()); |
||||
|
return false; |
||||
|
} |
||||
|
}, taskExecutor); |
||||
|
|
||||
|
future.thenAccept(hasCommits -> { |
||||
|
if (hasCommits) { |
||||
|
activeRepos.add(currentRepo); |
||||
|
} |
||||
|
int done = checked.incrementAndGet(); |
||||
|
if (done % 20 == 0 || done == totalRepos) { |
||||
|
log.debug("任务[{}] 快速检查进度: {}/{} | 活跃: {}", taskId, done, totalRepos, activeRepos.size()); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
futures.add(future); |
||||
|
} |
||||
|
|
||||
|
// 等待所有检查完成 |
||||
|
CompletableFuture<Void> allChecks = CompletableFuture.allOf( |
||||
|
futures.toArray(new CompletableFuture[0])); |
||||
|
|
||||
|
try { |
||||
|
allChecks.get(10, TimeUnit.SECONDS); |
||||
|
} catch (Exception e) { |
||||
|
log.warn("任务[{}] 部分仓库快速检查未完成: {}", taskId, e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
return activeRepos; |
||||
|
}, taskExecutor); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 详细分析活跃仓库 |
||||
|
*/ |
||||
|
private CompletableFuture<DetailedAnalysisResult> analyzeActiveRepositories(List<GiteaRepository> activeRepos, |
||||
|
ZonedDateTime sinceTime, |
||||
|
ZonedDateTime untilTime, |
||||
|
String taskId) { |
||||
|
return CompletableFuture.supplyAsync(() -> { |
||||
|
Map<String, DeveloperData> devDataMap = new ConcurrentHashMap<>(); |
||||
|
Map<String, RepoData> repoDataMap = new ConcurrentHashMap<>(); |
||||
|
Map<DayOfWeek, Integer> dayStats = new ConcurrentHashMap<>(); |
||||
|
Map<Integer, Integer> hourStats = new ConcurrentHashMap<>(); |
||||
|
Map<String, Integer> fileTypeStats = new ConcurrentHashMap<>(); |
||||
|
AtomicInteger totalCommits = new AtomicInteger(0); |
||||
|
AtomicInteger processed = new AtomicInteger(0); |
||||
|
|
||||
|
List<CompletableFuture<Void>> futures = activeRepos.stream() |
||||
|
.map(repo -> CompletableFuture.runAsync(() -> { |
||||
|
try { |
||||
|
analyzeSingleRepository(repo, sinceTime, untilTime, |
||||
|
devDataMap, repoDataMap, dayStats, hourStats, fileTypeStats, totalCommits); |
||||
|
} catch (Exception e) { |
||||
|
log.debug("任务[{}] 仓库 {} 详细分析失败: {}", taskId, repo.getFullPath(), e.getMessage()); |
||||
|
} finally { |
||||
|
int done = processed.incrementAndGet(); |
||||
|
if (done % 10 == 0 || done == activeRepos.size()) { |
||||
|
log.debug("任务[{}] 详细分析进度: {}/{} | 提交: {}", taskId, done, activeRepos.size(), totalCommits.get()); |
||||
|
} |
||||
|
} |
||||
|
}, taskExecutor)) |
||||
|
.collect(Collectors.toList()); |
||||
|
|
||||
|
// 等待所有分析完成 |
||||
|
CompletableFuture<Void> allFutures = CompletableFuture.allOf( |
||||
|
futures.toArray(new CompletableFuture[0])); |
||||
|
|
||||
|
try { |
||||
|
allFutures.get(15, TimeUnit.SECONDS); |
||||
|
} catch (Exception e) { |
||||
|
log.warn("任务[{}] 部分仓库详细分析未完成: {}", taskId, e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
return new DetailedAnalysisResult(devDataMap, repoDataMap, dayStats, hourStats, fileTypeStats, totalCommits.get()); |
||||
|
}, taskExecutor); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 分析单个仓库详情 |
||||
|
*/ |
||||
|
private void analyzeSingleRepository(GiteaRepository repo, |
||||
|
ZonedDateTime sinceTime, |
||||
|
ZonedDateTime untilTime, |
||||
|
Map<String, DeveloperData> devDataMap, |
||||
|
Map<String, RepoData> repoDataMap, |
||||
|
Map<DayOfWeek, Integer> dayStats, |
||||
|
Map<Integer, Integer> hourStats, |
||||
|
Map<String, Integer> fileTypeStats, |
||||
|
AtomicInteger totalCommits) throws Exception { |
||||
|
|
||||
|
if (Thread.currentThread().isInterrupted()) { |
||||
|
log.debug("任务被中断,跳过仓库分析: {}", repo.getFullPath()); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
String repoFullName = repo.getFullPath(); |
||||
|
List<GiteaCommit> commits = getCommitsInRange(repoFullName, sinceTime, untilTime); |
||||
|
|
||||
|
if (!commits.isEmpty()) { |
||||
|
RepoData repoData = new RepoData(); |
||||
|
repoData.repoName = repoFullName; |
||||
|
repoData.displayName = repo.getRepoName(); |
||||
|
|
||||
|
for (GiteaCommit commit : commits) { |
||||
|
if (Thread.currentThread().isInterrupted()) { |
||||
|
log.debug("处理提交时被中断"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
totalCommits.incrementAndGet(); |
||||
|
String author = getAuthorName(commit); |
||||
|
|
||||
|
// 更新开发者数据 |
||||
|
DeveloperData devData = devDataMap.computeIfAbsent(author, k -> new DeveloperData(author)); |
||||
|
devData.commitCount++; |
||||
|
devData.repos.add(repoFullName); |
||||
|
|
||||
|
// 更新仓库数据 |
||||
|
repoData.commitCount++; |
||||
|
repoData.developers.add(author); |
||||
|
|
||||
|
// 时间统计 |
||||
|
ZonedDateTime commitTime = commit.getCommitTime(); |
||||
|
if (commitTime != null) { |
||||
|
DayOfWeek day = commitTime.getDayOfWeek(); |
||||
|
int hour = commitTime.getHour(); |
||||
|
dayStats.merge(day, 1, Integer::sum); |
||||
|
hourStats.merge(hour, 1, Integer::sum); |
||||
|
} |
||||
|
|
||||
|
// 文件类型统计 |
||||
|
if (commit.getFiles() != null) { |
||||
|
for (GiteaCommit.ChangedFile file : commit.getFiles()) { |
||||
|
String fileType = file.getFileType(); |
||||
|
fileTypeStats.merge(fileType, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
repoDataMap.put(repoFullName, repoData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ==================== Gitea API调用方法 ==================== |
||||
|
|
||||
|
private boolean hasCommitsInRange(String repoFullName, ZonedDateTime since, ZonedDateTime until) throws Exception { |
||||
|
// 直接调用 getCommitsInRange 检查是否有提交 |
||||
|
List<GiteaCommit> commits = getCommitsInRange(repoFullName, since, until); |
||||
|
return !commits.isEmpty(); |
||||
|
} |
||||
|
|
||||
|
private List<GiteaCommit> getCommitsInRange(String repoFullName, ZonedDateTime since, ZonedDateTime until) throws Exception { |
||||
|
// 先获取最近3个月的提交(减少数据量) |
||||
|
LocalDateTime threeMonthsAgo = LocalDateTime.now().minusMonths(3); |
||||
|
ZonedDateTime recentSince = threeMonthsAgo.atZone(since.getZone()); |
||||
|
|
||||
|
String sinceStr = recentSince.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
String untilStr = until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
|
||||
|
String baseUrl = String.format("%s/api/v1/repos/%s/commits?since=%s&until=%s", |
||||
|
giteaBaseUrl, repoFullName, sinceStr, untilStr); |
||||
|
|
||||
|
List<GiteaCommit> recentCommits = fetchWithPagination(baseUrl, GiteaCommit.class, 8); |
||||
|
|
||||
|
// 在代码层面再次过滤到精确时间范围 |
||||
|
List<GiteaCommit> filteredCommits = new ArrayList<>(); |
||||
|
for (GiteaCommit commit : recentCommits) { |
||||
|
ZonedDateTime commitTime = commit.getCommitTime(); |
||||
|
if (commitTime != null && |
||||
|
!commitTime.isBefore(since) && |
||||
|
!commitTime.isAfter(until)) { |
||||
|
filteredCommits.add(commit); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return filteredCommits; |
||||
|
} |
||||
|
|
||||
|
private List<GiteaRepository> getAllUserRepositories() throws Exception { |
||||
|
String baseUrl = giteaBaseUrl + "/api/v1/user/repos?limit=50"; |
||||
|
return fetchWithPagination(baseUrl, GiteaRepository.class, 10); |
||||
|
} |
||||
|
|
||||
|
private <T> List<T> fetchWithPagination(String baseUrl, Class<T> clazz, int timeoutSeconds) throws Exception { |
||||
|
List<T> results = new ArrayList<>(); |
||||
|
int page = 1; |
||||
|
int maxPages = 10; |
||||
|
|
||||
|
while (page <= maxPages) { |
||||
|
if (Thread.currentThread().isInterrupted()) { |
||||
|
log.debug("分页获取被中断"); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
String pageUrl = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "page=" + page; |
||||
|
|
||||
|
HttpRequest request = HttpRequest.newBuilder() |
||||
|
.uri(java.net.URI.create(pageUrl)) |
||||
|
.header("Authorization", "token " + accessToken) |
||||
|
.timeout(Duration.ofSeconds(timeoutSeconds)) |
||||
|
.GET() |
||||
|
.build(); |
||||
|
|
||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); |
||||
|
|
||||
|
if (response.statusCode() != 200) { |
||||
|
log.warn("API请求失败: {} - {}", response.statusCode(), response.body()); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
List<T> pageResults = objectMapper.readValue( |
||||
|
response.body(), |
||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); |
||||
|
|
||||
|
if (pageResults.isEmpty()) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
results.addAll(pageResults); |
||||
|
|
||||
|
if (pageResults.size() < 50) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
page++; |
||||
|
} |
||||
|
|
||||
|
return results; |
||||
|
} |
||||
|
|
||||
|
private String getAuthorName(GiteaCommit commit) { |
||||
|
if (commit.getCommit() != null && |
||||
|
commit.getCommit().getAuthor() != null && |
||||
|
commit.getCommit().getAuthor().getName() != null) { |
||||
|
return commit.getCommit().getAuthor().getName(); |
||||
|
} |
||||
|
return "未知作者"; |
||||
|
} |
||||
|
|
||||
|
// ==================== GitAnalysisData构建方法 ==================== |
||||
|
|
||||
|
/** |
||||
|
* 构建完整的GitAnalysisData |
||||
|
*/ |
||||
|
private GitAnalysisData buildGitAnalysisData(String since, String until, int totalRepos, int activeRepos, |
||||
|
DetailedAnalysisResult detailResult, long analysisTime) { |
||||
|
GitAnalysisData data = new GitAnalysisData(); |
||||
|
|
||||
|
// 基础信息 |
||||
|
data.setBasicInfo(new BasicInfo( |
||||
|
since + " 至 " + until, |
||||
|
totalRepos, |
||||
|
activeRepos, |
||||
|
detailResult.getDevDataMap().size(), |
||||
|
detailResult.getTotalCommits(), |
||||
|
analysisTime, |
||||
|
"快速筛选 + 详细分析" |
||||
|
)); |
||||
|
|
||||
|
// 开发者排行榜 |
||||
|
if (!detailResult.getDevDataMap().isEmpty()) { |
||||
|
List<DeveloperData> devList = new ArrayList<>(detailResult.getDevDataMap().values()); |
||||
|
devList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
|
||||
|
List<DeveloperRank> developerRanks = new ArrayList<>(); |
||||
|
int rank = 1; |
||||
|
for (DeveloperData dev : devList) { |
||||
|
if (rank > 10) break; |
||||
|
developerRanks.add(new DeveloperRank(rank++, dev.name, dev.commitCount, dev.repos.size())); |
||||
|
} |
||||
|
data.setDeveloperRanks(developerRanks); |
||||
|
} |
||||
|
|
||||
|
// 仓库排行榜 |
||||
|
if (!detailResult.getRepoDataMap().isEmpty()) { |
||||
|
List<RepoData> repoList = new ArrayList<>(detailResult.getRepoDataMap().values()); |
||||
|
repoList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
|
||||
|
List<RepoRank> repoRanks = new ArrayList<>(); |
||||
|
int rank = 1; |
||||
|
for (RepoData repo : repoList) { |
||||
|
if (rank > 10) break; |
||||
|
repoRanks.add(new RepoRank(rank++, repo.repoName, repo.displayName, repo.commitCount, repo.developers.size())); |
||||
|
} |
||||
|
data.setRepoRanks(repoRanks); |
||||
|
} |
||||
|
|
||||
|
// 时间分布(按星期) |
||||
|
if (!detailResult.getDayStats().isEmpty()) { |
||||
|
String[] dayNames = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; |
||||
|
DayOfWeek[] days = DayOfWeek.values(); |
||||
|
|
||||
|
List<DayStats> dayStatsList = new ArrayList<>(); |
||||
|
for (int i = 0; i < 7; i++) { |
||||
|
int count = detailResult.getDayStats().getOrDefault(days[i], 0); |
||||
|
dayStatsList.add(new DayStats(dayNames[i], count)); |
||||
|
} |
||||
|
data.setDayStats(dayStatsList); |
||||
|
} |
||||
|
|
||||
|
// 文件类型统计 |
||||
|
if (!detailResult.getFileTypeStats().isEmpty()) { |
||||
|
List<Map.Entry<String, Integer>> fileList = new ArrayList<>(detailResult.getFileTypeStats().entrySet()); |
||||
|
fileList.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); |
||||
|
|
||||
|
List<FileTypeStats> fileTypeStatsList = new ArrayList<>(); |
||||
|
for (Map.Entry<String, Integer> entry : fileList) { |
||||
|
fileTypeStatsList.add(new FileTypeStats(entry.getKey(), entry.getValue())); |
||||
|
} |
||||
|
data.setFileTypeStats(fileTypeStatsList); |
||||
|
} |
||||
|
|
||||
|
// 生成时间 |
||||
|
data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建空的GitAnalysisData(无仓库时) |
||||
|
*/ |
||||
|
private GitAnalysisData buildEmptyGitAnalysisData(String message) { |
||||
|
GitAnalysisData data = new GitAnalysisData(); |
||||
|
data.setBasicInfo(new BasicInfo( |
||||
|
"无时间范围", |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
"无仓库数据" |
||||
|
)); |
||||
|
data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建简单的GitAnalysisData(无活跃仓库时) |
||||
|
*/ |
||||
|
private GitAnalysisData buildSimpleGitAnalysisData(String since, String until, int totalRepos, |
||||
|
int activeRepos, int totalDevs, long startTime) { |
||||
|
long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
|
||||
|
GitAnalysisData data = new GitAnalysisData(); |
||||
|
data.setBasicInfo(new BasicInfo( |
||||
|
since + " 至 " + until, |
||||
|
totalRepos, |
||||
|
activeRepos, |
||||
|
totalDevs, |
||||
|
0, |
||||
|
analysisTime, |
||||
|
"快速筛选" |
||||
|
)); |
||||
|
data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
// ==================== 内部数据类 ==================== |
||||
|
|
||||
|
private static class DeveloperData { |
||||
|
String name; |
||||
|
int commitCount = 0; |
||||
|
Set<String> repos = new HashSet<>(); |
||||
|
|
||||
|
DeveloperData(String name) { |
||||
|
this.name = name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static class RepoData { |
||||
|
String repoName; |
||||
|
String displayName; |
||||
|
int commitCount = 0; |
||||
|
Set<String> developers = new HashSet<>(); |
||||
|
} |
||||
|
|
||||
|
// ==================== 详细分析结果类 ==================== |
||||
|
|
||||
|
private static class DetailedAnalysisResult { |
||||
|
private final Map<String, DeveloperData> devDataMap; |
||||
|
private final Map<String, RepoData> repoDataMap; |
||||
|
private final Map<DayOfWeek, Integer> dayStats; |
||||
|
private final Map<Integer, Integer> hourStats; |
||||
|
private final Map<String, Integer> fileTypeStats; |
||||
|
private final int totalCommits; |
||||
|
|
||||
|
public DetailedAnalysisResult(Map<String, DeveloperData> devDataMap, Map<String, RepoData> repoDataMap, |
||||
|
Map<DayOfWeek, Integer> dayStats, Map<Integer, Integer> hourStats, |
||||
|
Map<String, Integer> fileTypeStats, int totalCommits) { |
||||
|
this.devDataMap = devDataMap; |
||||
|
this.repoDataMap = repoDataMap; |
||||
|
this.dayStats = dayStats; |
||||
|
this.hourStats = hourStats; |
||||
|
this.fileTypeStats = fileTypeStats; |
||||
|
this.totalCommits = totalCommits; |
||||
|
} |
||||
|
|
||||
|
public Map<String, DeveloperData> getDevDataMap() { return devDataMap; } |
||||
|
public Map<String, RepoData> getRepoDataMap() { return repoDataMap; } |
||||
|
public Map<DayOfWeek, Integer> getDayStats() { return dayStats; } |
||||
|
public Map<Integer, Integer> getHourStats() { return hourStats; } |
||||
|
public Map<String, Integer> getFileTypeStats() { return fileTypeStats; } |
||||
|
public int getTotalCommits() { return totalCommits; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,398 @@ |
|||||
|
//package com.chenhai.chenhaiai.service; |
||||
|
// |
||||
|
//import com.chenhai.chenhaiai.entity.AnalysisResult; |
||||
|
//import com.chenhai.chenhaiai.entity.git.GiteaCommit; |
||||
|
// |
||||
|
//import java.io.ByteArrayOutputStream; |
||||
|
//import java.io.PrintStream; |
||||
|
//import java.time.format.DateTimeFormatter; |
||||
|
//import java.util.*; |
||||
|
// |
||||
|
///** |
||||
|
// * Gitea分析报告生成器 - 优化版,解决日志干扰问题 |
||||
|
// */ |
||||
|
//public class GiteaAnalysisTest { |
||||
|
// |
||||
|
// private final GiteaAnalysisService analysisService; |
||||
|
// |
||||
|
// public GiteaAnalysisTest(String giteaBaseUrl, String accessToken) { |
||||
|
// this.analysisService = new GiteaAnalysisService(giteaBaseUrl, accessToken); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 生成完整上周报告 - 优化版,避免日志干扰 |
||||
|
// */ |
||||
|
// public void generateCompleteReport() { |
||||
|
// // 保存原始输出流 |
||||
|
// PrintStream originalOut = System.out; |
||||
|
// PrintStream originalErr = System.err; |
||||
|
// |
||||
|
// // 创建一个缓冲区来收集所有输出 |
||||
|
// ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||
|
// PrintStream bufferStream = new PrintStream(baos); |
||||
|
// |
||||
|
// try { |
||||
|
// // 重定向System.out到缓冲区 |
||||
|
// System.setOut(bufferStream); |
||||
|
// |
||||
|
// System.out.println("\n" + "=".repeat(80)); |
||||
|
// System.out.println("🚀 GITEA 上周开发活动分析报告"); |
||||
|
// System.out.println("=".repeat(80)); |
||||
|
// System.out.println("生成时间: " + java.time.LocalDateTime.now().format( |
||||
|
// DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
// System.out.println("=".repeat(80) + "\n"); |
||||
|
// |
||||
|
// System.out.println("📅 分析时间范围: 上周一 00:00 至 周日 23:59"); |
||||
|
// System.out.println("🕐 正在获取数据...\n"); |
||||
|
// |
||||
|
// // 1. 执行完整分析 |
||||
|
// AnalysisResult result = analysisService.analyzeLastWeek(); |
||||
|
// |
||||
|
// // 2. 生成报告 |
||||
|
// generateReport(result); |
||||
|
// |
||||
|
// // 3. 生成单个仓库详细报告(如果有活跃仓库) |
||||
|
// if (!result.getRepositoryRanking().isEmpty()) { |
||||
|
// String topRepo = result.getRepositoryRanking().get(0).getKey(); |
||||
|
// System.out.println("\n" + "=".repeat(80)); |
||||
|
// System.out.println("📋 最活跃仓库详细分析: " + topRepo); |
||||
|
// System.out.println("=".repeat(80)); |
||||
|
// generateRepoDetailReport(topRepo); |
||||
|
// } |
||||
|
// |
||||
|
// System.out.println("\n" + "=".repeat(80)); |
||||
|
// System.out.println("✅ 报告生成完成!"); |
||||
|
// System.out.println("=".repeat(80)); |
||||
|
// |
||||
|
// } catch (Exception e) { |
||||
|
// System.err.println("\n❌ 分析失败: " + e.getMessage()); |
||||
|
// e.printStackTrace(); |
||||
|
// } finally { |
||||
|
// // 恢复原始输出流 |
||||
|
// System.setOut(originalOut); |
||||
|
// System.setErr(originalErr); |
||||
|
// |
||||
|
// // 从缓冲区获取完整的报告内容 |
||||
|
// String fullReport = baos.toString(); |
||||
|
// |
||||
|
// // 清理报告:移除日志行 |
||||
|
// String cleanedReport = cleanReport(fullReport); |
||||
|
// |
||||
|
// // 输出纯净的报告 |
||||
|
// originalOut.println(cleanedReport); |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 清理报告,移除日志行 |
||||
|
// */ |
||||
|
// private String cleanReport(String fullReport) { |
||||
|
// if (fullReport == null || fullReport.isEmpty()) { |
||||
|
// return fullReport; |
||||
|
// } |
||||
|
// |
||||
|
// StringBuilder cleaned = new StringBuilder(); |
||||
|
// String[] lines = fullReport.split("\n"); |
||||
|
// |
||||
|
// // 保留分析过程的状态指示,但移除详细的日志信息 |
||||
|
// boolean inProgressSection = false; |
||||
|
// |
||||
|
// for (String line : lines) { |
||||
|
// // 判断是否是日志行(包含时间戳和日志级别) |
||||
|
// if (isLogLine(line)) { |
||||
|
// // 如果包含重要进度信息,可以保留简化版本 |
||||
|
// if (line.contains("检查进度:") || line.contains("分析完成") || |
||||
|
// line.contains("活跃仓库数:") || line.contains("开发者活动分析完成")) { |
||||
|
// // 提取简化信息 |
||||
|
// String simplified = extractProgressInfo(line); |
||||
|
// if (simplified != null) { |
||||
|
// cleaned.append(simplified).append("\n"); |
||||
|
// } |
||||
|
// } |
||||
|
// // 跳过其他详细的日志行 |
||||
|
// continue; |
||||
|
// } |
||||
|
// |
||||
|
// // 保留所有非日志行(真正的报告内容) |
||||
|
// cleaned.append(line).append("\n"); |
||||
|
// } |
||||
|
// |
||||
|
// return cleaned.toString(); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 判断是否是日志行 |
||||
|
// */ |
||||
|
// private boolean isLogLine(String line) { |
||||
|
// // 日志行通常包含时间戳模式(如 20:24:28.848)和日志级别(INFO、WARN、DEBUG、ERROR) |
||||
|
// return line.matches("^\\d{2}:\\d{2}:\\d{2}\\.\\d{3}.*") && |
||||
|
// (line.contains("INFO") || line.contains("WARN") || |
||||
|
// line.contains("DEBUG") || line.contains("ERROR")); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 从日志行中提取进度信息 |
||||
|
// */ |
||||
|
// private String extractProgressInfo(String logLine) { |
||||
|
// if (logLine.contains("检查进度:")) { |
||||
|
// // 提取如 "检查进度: 10/182 (活跃: 0 个)" 这样的信息 |
||||
|
// String[] parts = logLine.split("检查进度:"); |
||||
|
// if (parts.length > 1) { |
||||
|
// String progress = parts[1].split("\\[")[0].trim(); // 移除线程信息 |
||||
|
// return "📊 " + progress; |
||||
|
// } |
||||
|
// } else if (logLine.contains("✅ 检查完成")) { |
||||
|
// return "✅ 仓库检查完成"; |
||||
|
// } else if (logLine.contains("活跃仓库数:")) { |
||||
|
// String[] parts = logLine.split("活跃仓库数:"); |
||||
|
// if (parts.length > 1) { |
||||
|
// return "📦 " + parts[1].split("\\[")[0].trim(); |
||||
|
// } |
||||
|
// } else if (logLine.contains("开发者活动分析完成")) { |
||||
|
// return "👥 开发者分析完成"; |
||||
|
// } else if (logLine.contains("仓库活动分析完成")) { |
||||
|
// return "🏢 仓库活动分析完成"; |
||||
|
// } else if (logLine.contains("分析完成,结果ID:")) { |
||||
|
// return "🎯 数据分析完成"; |
||||
|
// } |
||||
|
// return null; |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 生成主报告 - 保持不变 |
||||
|
// */ |
||||
|
// private void generateReport(AnalysisResult result) { |
||||
|
// if (result == null) { |
||||
|
// System.out.println("⚠️ 分析结果为空,无法生成报告"); |
||||
|
// return; |
||||
|
// } |
||||
|
// |
||||
|
// // 1. 概览统计 |
||||
|
// System.out.println("📊 概览统计"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// System.out.printf("%-25s: %d 个\n", "总仓库数", result.getTotalRepositories()); |
||||
|
// System.out.printf("%-25s: %d 个\n", "活跃仓库数", result.getActiveRepositories()); |
||||
|
// System.out.printf("%-25s: %d 人\n", "活跃开发者数", result.getTotalDevelopers()); |
||||
|
// System.out.printf("%-25s: %d 次\n", "总提交次数", result.getTotalCommits()); |
||||
|
// |
||||
|
// if (result.getTotalCommits() == 0) { |
||||
|
// System.out.println("\n📭 上周没有提交记录"); |
||||
|
// return; |
||||
|
// } |
||||
|
// |
||||
|
// // 2. 开发者排行榜 |
||||
|
// if (!result.getDeveloperRanking().isEmpty()) { |
||||
|
// System.out.println("\n👨💻 开发者贡献排行榜"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// System.out.printf("%-4s %-20s %-10s %-12s %-15s\n", |
||||
|
// "排名", "开发者", "提交次数", "参与仓库", "最活跃时段"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// |
||||
|
// int rank = 1; |
||||
|
// for (var entry : result.getDeveloperRanking()) { |
||||
|
// if (rank > 15) break; |
||||
|
// |
||||
|
// var activity = entry.getValue(); |
||||
|
// System.out.printf("%-4d %-20s %-10d %-12d %-15s\n", |
||||
|
// rank++, |
||||
|
// truncate(entry.getKey(), 20), |
||||
|
// activity.getTotalCommits(), |
||||
|
// activity.getContributedRepos().size(), |
||||
|
// activity.getMostActiveDay().substring(0, 3) + " " + activity.getMostActiveHour()); |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// // 3. 仓库排行榜 |
||||
|
// if (!result.getRepositoryRanking().isEmpty()) { |
||||
|
// System.out.println("\n🏢 活跃仓库排行榜"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// System.out.printf("%-4s %-40s %-10s %-10s %-15s\n", |
||||
|
// "排名", "仓库名称", "提交次数", "开发者数", "主要文件类型"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// |
||||
|
// int rank = 1; |
||||
|
// for (var entry : result.getRepositoryRanking()) { |
||||
|
// if (rank > 10) break; |
||||
|
// |
||||
|
// var activity = entry.getValue(); |
||||
|
// System.out.printf("%-4d %-40s %-10d %-10d %-15s\n", |
||||
|
// rank++, |
||||
|
// truncate(entry.getKey(), 40), |
||||
|
// activity.getTotalCommits(), |
||||
|
// activity.getDeveloperCount(), |
||||
|
// activity.getMostChangedFileType()); |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// // 4. 时间分布分析 |
||||
|
// if (!result.getOverallCommitsByDay().isEmpty()) { |
||||
|
// System.out.println("\n⏰ 提交时间分布"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// |
||||
|
// String[] days = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; |
||||
|
// java.time.DayOfWeek[] dayOfWeeks = { |
||||
|
// java.time.DayOfWeek.MONDAY, java.time.DayOfWeek.TUESDAY, |
||||
|
// java.time.DayOfWeek.WEDNESDAY, java.time.DayOfWeek.THURSDAY, |
||||
|
// java.time.DayOfWeek.FRIDAY, java.time.DayOfWeek.SATURDAY, |
||||
|
// java.time.DayOfWeek.SUNDAY |
||||
|
// }; |
||||
|
// |
||||
|
// int maxCommits = result.getOverallCommitsByDay().values().stream() |
||||
|
// .max(Integer::compareTo).orElse(1); |
||||
|
// |
||||
|
// for (int i = 0; i < days.length; i++) { |
||||
|
// int commits = result.getOverallCommitsByDay().getOrDefault(dayOfWeeks[i], 0); |
||||
|
// int barLength = maxCommits > 0 ? (commits * 40 / maxCommits) : 0; |
||||
|
// String bar = "█".repeat(barLength); |
||||
|
// |
||||
|
// System.out.printf("%-4s: %3d 次提交 | %-40s\n", |
||||
|
// days[i], commits, bar); |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// // 5. 文件类型分析 |
||||
|
// if (!result.getOverallFileTypeDistribution().isEmpty()) { |
||||
|
// System.out.println("\n📁 文件类型分布"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// |
||||
|
// int totalFiles = result.getOverallFileTypeDistribution().values().stream() |
||||
|
// .mapToInt(Integer::intValue).sum(); |
||||
|
// |
||||
|
// int maxFiles = result.getOverallFileTypeDistribution().values().stream() |
||||
|
// .max(Integer::compareTo).orElse(1); |
||||
|
// |
||||
|
// result.getOverallFileTypeDistribution().entrySet().stream() |
||||
|
// .sorted((a, b) -> Integer.compare(b.getValue(), a.getValue())) |
||||
|
// .forEach(entry -> { |
||||
|
// int count = entry.getValue(); |
||||
|
// int barLength = maxFiles > 0 ? (count * 40 / maxFiles) : 0; |
||||
|
// String bar = "█".repeat(barLength); |
||||
|
// double percentage = totalFiles > 0 ? (count * 100.0 / totalFiles) : 0; |
||||
|
// |
||||
|
// System.out.printf("%-10s: %3d 次修改 (%5.1f%%) | %-40s\n", |
||||
|
// entry.getKey(), count, percentage, bar); |
||||
|
// }); |
||||
|
// } |
||||
|
// |
||||
|
// // 6. 提交摘要 |
||||
|
// if (!result.getDeveloperRanking().isEmpty()) { |
||||
|
// System.out.println("\n💬 提交摘要"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// |
||||
|
// // 获取所有提交消息 |
||||
|
// List<String> allMessages = new ArrayList<>(); |
||||
|
// for (var entry : result.getDeveloperRanking()) { |
||||
|
// var activity = entry.getValue(); |
||||
|
// allMessages.addAll(activity.getRecentCommitMessages()); |
||||
|
// } |
||||
|
// |
||||
|
// if (!allMessages.isEmpty()) { |
||||
|
// System.out.println("📝 最近的提交消息:"); |
||||
|
// allMessages.stream() |
||||
|
// .filter(msg -> msg != null && !msg.trim().isEmpty()) |
||||
|
// .distinct() |
||||
|
// .limit(8) |
||||
|
// .forEach(msg -> { |
||||
|
// System.out.printf(" • %s\n", truncate(msg.trim(), 70)); |
||||
|
// }); |
||||
|
// } |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 生成仓库详细报告 - 保持不变 |
||||
|
// */ |
||||
|
// private void generateRepoDetailReport(String repoFullName) { |
||||
|
// try { |
||||
|
// String since = "2025-12-01T00:00:00+08:00"; |
||||
|
// String until = "2025-12-07T23:59:59+08:00"; |
||||
|
// |
||||
|
// var commits = analysisService.getRepositoryCommits(repoFullName, since, until); |
||||
|
// |
||||
|
// if (commits.isEmpty()) { |
||||
|
// System.out.println("该时间段内没有提交记录"); |
||||
|
// return; |
||||
|
// } |
||||
|
// |
||||
|
// System.out.printf("📊 提交统计: %d 次提交\n", commits.size()); |
||||
|
// |
||||
|
// // 按开发者统计 |
||||
|
// Map<String, List<GiteaCommit>> commitsByDeveloper = new HashMap<>(); |
||||
|
// for (var commit : commits) { |
||||
|
// if (commit.getCommit() != null && commit.getCommit().getAuthor() != null) { |
||||
|
// String developer = commit.getCommit().getAuthor().getName(); |
||||
|
// if (developer == null || developer.isEmpty()) { |
||||
|
// developer = commit.getCommit().getAuthor().getEmail(); |
||||
|
// } |
||||
|
// commitsByDeveloper.computeIfAbsent(developer, k -> new ArrayList<>()).add(commit); |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// System.out.println("\n👥 开发者贡献:"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// |
||||
|
// commitsByDeveloper.entrySet().stream() |
||||
|
// .sorted((a, b) -> Integer.compare(b.getValue().size(), a.getValue().size())) |
||||
|
// .forEach(entry -> { |
||||
|
// int count = entry.getValue().size(); |
||||
|
// double percentage = (count * 100.0) / commits.size(); |
||||
|
// System.out.printf(" %-20s: %3d 次提交 (%5.1f%%)\n", |
||||
|
// truncate(entry.getKey(), 20), count, percentage); |
||||
|
// }); |
||||
|
// |
||||
|
// // 最近提交记录 |
||||
|
// System.out.println("\n🕐 最近提交记录:"); |
||||
|
// System.out.println("-".repeat(60)); |
||||
|
// |
||||
|
// commits.stream() |
||||
|
// .sorted((a, b) -> b.getCommitTime().compareTo(a.getCommitTime())) |
||||
|
// .limit(8) |
||||
|
// .forEach(commit -> { |
||||
|
// String time = commit.getCommitTime().format( |
||||
|
// DateTimeFormatter.ofPattern("MM-dd HH:mm")); |
||||
|
// String author = commit.getCommit().getAuthor().getName(); |
||||
|
// if (author == null || author.isEmpty()) { |
||||
|
// author = commit.getCommit().getAuthor().getEmail(); |
||||
|
// } |
||||
|
// |
||||
|
// System.out.printf("[%s] %-15s | %s\n", |
||||
|
// time, |
||||
|
// truncate(author, 15), |
||||
|
// truncate(commit.getShortMessage(), 50)); |
||||
|
// }); |
||||
|
// |
||||
|
// } catch (Exception e) { |
||||
|
// System.err.println("生成详细报告失败: " + e.getMessage()); |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 字符串截断 |
||||
|
// */ |
||||
|
// private String truncate(String str, int maxLength) { |
||||
|
// if (str == null) return ""; |
||||
|
// if (str.length() <= maxLength) return str; |
||||
|
// return str.substring(0, maxLength - 3) + "..."; |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 主方法 - 直接运行生成完整报告 |
||||
|
// */ |
||||
|
// public static void main(String[] args) { |
||||
|
// try { |
||||
|
// // 配置 |
||||
|
// String giteaBaseUrl = "http://192.168.1.224:3000"; |
||||
|
// String accessToken = "a9f1c8d3d6fefd73956604f496457faaa3672f89"; |
||||
|
// |
||||
|
// // 创建报告生成器 |
||||
|
// GiteaAnalysisTest generator = new GiteaAnalysisTest(giteaBaseUrl, accessToken); |
||||
|
// |
||||
|
// // 直接生成完整报告(优化版) |
||||
|
// generator.generateCompleteReport(); |
||||
|
// |
||||
|
// } catch (Exception e) { |
||||
|
// System.err.println("❌ 程序异常: " + e.getMessage()); |
||||
|
// e.printStackTrace(); |
||||
|
// } |
||||
|
// } |
||||
|
//} |
||||
@ -0,0 +1,605 @@ |
|||||
|
//package com.chenhai.chenhaiai.service; |
||||
|
// |
||||
|
//import com.chenhai.chenhaiai.entity.git.*; |
||||
|
//import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
//import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
|
//import jakarta.annotation.PostConstruct; |
||||
|
//import lombok.extern.slf4j.Slf4j; |
||||
|
//import org.springframework.beans.factory.annotation.Autowired; |
||||
|
//import org.springframework.beans.factory.annotation.Value; |
||||
|
//import org.springframework.core.task.TaskExecutor; |
||||
|
//import org.springframework.scheduling.annotation.Async; |
||||
|
//import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
||||
|
//import org.springframework.stereotype.Service; |
||||
|
// |
||||
|
//import java.net.http.HttpClient; |
||||
|
//import java.net.http.HttpRequest; |
||||
|
//import java.net.http.HttpResponse; |
||||
|
//import java.time.DayOfWeek; |
||||
|
//import java.time.Duration; |
||||
|
//import java.time.LocalDateTime; |
||||
|
//import java.time.ZonedDateTime; |
||||
|
//import java.time.format.DateTimeFormatter; |
||||
|
//import java.util.*; |
||||
|
//import java.util.concurrent.CompletableFuture; |
||||
|
//import java.util.concurrent.ConcurrentHashMap; |
||||
|
//import java.util.concurrent.TimeUnit; |
||||
|
//import java.util.concurrent.atomic.AtomicInteger; |
||||
|
//import java.util.stream.Collectors; |
||||
|
// |
||||
|
///** |
||||
|
// * 长时间范围Git分析服务 |
||||
|
// * 专门处理任意时间范围的Git分析,不影响原GiteaAnalysisService |
||||
|
// */ |
||||
|
//@Slf4j |
||||
|
//@Service |
||||
|
//public class LongTermGiteaAnalysisService { |
||||
|
// |
||||
|
// @Value("${gitea.url:http://192.168.1.224:3000}") |
||||
|
// private String giteaBaseUrl; |
||||
|
// |
||||
|
// @Value("${gitea.token:a9f1c8d3d6fefd73956604f496457faaa3672f89}") |
||||
|
// private String accessToken; |
||||
|
// |
||||
|
// @Value("${gitea.longterm.max-active-repos:50}") |
||||
|
// private int maxActiveRepos; |
||||
|
// |
||||
|
// @Value("${gitea.longterm.commit-timeout-seconds:30}") |
||||
|
// private int commitTimeoutSeconds; |
||||
|
// |
||||
|
// @Value("${gitea.longterm.max-pages:30}") |
||||
|
// private int maxPages; |
||||
|
// |
||||
|
// @Autowired |
||||
|
// private ThreadPoolTaskExecutor giteaTaskExecutor; |
||||
|
// |
||||
|
// @Autowired |
||||
|
// private TaskExecutor taskExecutor; |
||||
|
// |
||||
|
// private HttpClient httpClient; |
||||
|
// private ObjectMapper objectMapper; |
||||
|
// |
||||
|
// @PostConstruct |
||||
|
// public void init() { |
||||
|
// log.info("初始化长时间范围Gitea分析服务..."); |
||||
|
// |
||||
|
// httpClient = HttpClient.newBuilder() |
||||
|
// .connectTimeout(Duration.ofSeconds(15)) |
||||
|
// .build(); |
||||
|
// |
||||
|
// objectMapper = new ObjectMapper(); |
||||
|
// objectMapper.registerModule(new JavaTimeModule()); |
||||
|
// |
||||
|
// log.info("长时间范围Gitea分析服务初始化完成"); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 异步执行长时间范围Git分析 |
||||
|
// * @param since 开始时间 |
||||
|
// * @param until 结束时间 |
||||
|
// * @return 结构化分析数据 |
||||
|
// */ |
||||
|
// @Async("giteaTaskExecutor") |
||||
|
// public CompletableFuture<GitAnalysisData> analyzeLongTermGitDataAsync(String since, String until) { |
||||
|
// String taskId = UUID.randomUUID().toString().substring(0, 8); |
||||
|
// long startTime = System.currentTimeMillis(); |
||||
|
// |
||||
|
// log.info("开始长时间Git分析任务[{}]: {} 至 {}", taskId, since, until); |
||||
|
// |
||||
|
// return CompletableFuture.supplyAsync(() -> { |
||||
|
// try { |
||||
|
// // 验证时间范围 |
||||
|
// ZonedDateTime sinceTime = ZonedDateTime.parse(since); |
||||
|
// ZonedDateTime untilTime = ZonedDateTime.parse(until); |
||||
|
// |
||||
|
// Duration duration = Duration.between(sinceTime, untilTime); |
||||
|
// log.info("任务[{}] 分析时间范围: {} 天", taskId, duration.toDays()); |
||||
|
// |
||||
|
// if (duration.toDays() > 365) { |
||||
|
// log.warn("任务[{}] 时间范围超过一年,性能可能受影响", taskId); |
||||
|
// } |
||||
|
// |
||||
|
// GitAnalysisData analysisData = performLongTermAnalysis(since, until, taskId); |
||||
|
// long cost = System.currentTimeMillis() - startTime; |
||||
|
// log.info("长时间Git分析任务[{}]完成,耗时: {}ms", taskId, cost); |
||||
|
// return analysisData; |
||||
|
// } catch (Exception e) { |
||||
|
// log.error("长时间Git分析任务[{}]失败: {}", taskId, e.getMessage(), e); |
||||
|
// throw new RuntimeException("长时间Git分析失败: " + e.getMessage(), e); |
||||
|
// } |
||||
|
// }, giteaTaskExecutor); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 执行长时间范围完整分析 |
||||
|
// */ |
||||
|
// private GitAnalysisData performLongTermAnalysis(String since, String until, String taskId) throws Exception { |
||||
|
// long startTime = System.currentTimeMillis(); |
||||
|
// |
||||
|
// // 1. 获取所有仓库 |
||||
|
// log.debug("长时间任务[{}] 获取仓库列表...", taskId); |
||||
|
// List<GiteaRepository> allRepos = getAllUserRepositories(); |
||||
|
// int totalRepos = allRepos.size(); |
||||
|
// |
||||
|
// if (totalRepos == 0) { |
||||
|
// return buildEmptyGitAnalysisData("无仓库数据"); |
||||
|
// } |
||||
|
// |
||||
|
// log.info("长时间任务[{}] 发现仓库: {} 个", taskId, totalRepos); |
||||
|
// |
||||
|
// // 2. 解析时间范围 |
||||
|
// ZonedDateTime sinceTime = ZonedDateTime.parse(since); |
||||
|
// ZonedDateTime untilTime = ZonedDateTime.parse(until); |
||||
|
// |
||||
|
// // 3. 快速筛选活跃仓库(使用优化方法) |
||||
|
// log.debug("长时间任务[{}] 快速筛选活跃仓库...", taskId); |
||||
|
// List<GiteaRepository> activeRepos = findLongTermActiveRepositories(allRepos, sinceTime, untilTime, taskId) |
||||
|
// .get(30, TimeUnit.SECONDS); // 增加超时时间 |
||||
|
// int activeRepoCount = activeRepos.size(); |
||||
|
// |
||||
|
// log.info("长时间任务[{}] 活跃仓库: {} 个", taskId, activeRepoCount); |
||||
|
// |
||||
|
// if (activeRepoCount == 0) { |
||||
|
// return buildSimpleGitAnalysisData(since, until, totalRepos, 0, 0, startTime); |
||||
|
// } |
||||
|
// |
||||
|
// // 限制活跃仓库数量 |
||||
|
// if (activeRepoCount > maxActiveRepos) { |
||||
|
// log.warn("长时间任务[{}] 活跃仓库过多({}个),采样分析前{}个", taskId, activeRepoCount, maxActiveRepos); |
||||
|
// activeRepos = activeRepos.subList(0, maxActiveRepos); |
||||
|
// activeRepoCount = maxActiveRepos; |
||||
|
// } |
||||
|
// |
||||
|
// // 4. 详细分析活跃仓库 |
||||
|
// log.debug("长时间任务[{}] 详细分析活跃仓库...", taskId); |
||||
|
// DetailedAnalysisResult detailResult = analyzeLongTermActiveRepositories(activeRepos, sinceTime, untilTime, taskId) |
||||
|
// .get(60, TimeUnit.SECONDS); // 长时间分析,增加超时 |
||||
|
// |
||||
|
// // 5. 构建结构化数据 |
||||
|
// long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
// return buildGitAnalysisData(since, until, totalRepos, activeRepoCount, detailResult, analysisTime); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 长时间范围快速筛选活跃仓库(优化版) |
||||
|
// */ |
||||
|
// private CompletableFuture<List<GiteaRepository>> findLongTermActiveRepositories(List<GiteaRepository> allRepos, |
||||
|
// ZonedDateTime since, |
||||
|
// ZonedDateTime until, |
||||
|
// String taskId) { |
||||
|
// return CompletableFuture.supplyAsync(() -> { |
||||
|
// List<GiteaRepository> activeRepos = Collections.synchronizedList(new ArrayList<>()); |
||||
|
// List<CompletableFuture<Boolean>> futures = new ArrayList<>(); |
||||
|
// AtomicInteger checked = new AtomicInteger(0); |
||||
|
// final int totalRepos = allRepos.size(); |
||||
|
// |
||||
|
// for (GiteaRepository repo : allRepos) { |
||||
|
// final GiteaRepository currentRepo = repo; |
||||
|
// |
||||
|
// CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> { |
||||
|
// try { |
||||
|
// // 使用优化版的快速检查方法 |
||||
|
// return hasCommitsInLongTermRange(currentRepo.getFullPath(), since, until); |
||||
|
// } catch (Exception e) { |
||||
|
// log.debug("长时间任务[{}] 仓库 {} 快速检查失败: {}", taskId, currentRepo.getFullPath(), e.getMessage()); |
||||
|
// return false; |
||||
|
// } |
||||
|
// }, taskExecutor); |
||||
|
// |
||||
|
// future.thenAccept(hasCommits -> { |
||||
|
// if (hasCommits) { |
||||
|
// activeRepos.add(currentRepo); |
||||
|
// } |
||||
|
// int done = checked.incrementAndGet(); |
||||
|
// if (done % 20 == 0 || done == totalRepos) { |
||||
|
// log.debug("长时间任务[{}] 快速检查进度: {}/{} | 活跃: {}", taskId, done, totalRepos, activeRepos.size()); |
||||
|
// } |
||||
|
// }); |
||||
|
// |
||||
|
// futures.add(future); |
||||
|
// } |
||||
|
// |
||||
|
// CompletableFuture<Void> allChecks = CompletableFuture.allOf( |
||||
|
// futures.toArray(new CompletableFuture[0])); |
||||
|
// |
||||
|
// try { |
||||
|
// allChecks.get(15, TimeUnit.SECONDS); |
||||
|
// } catch (Exception e) { |
||||
|
// log.warn("长时间任务[{}] 部分仓库快速检查未完成: {}", taskId, e.getMessage()); |
||||
|
// } |
||||
|
// |
||||
|
// return activeRepos; |
||||
|
// }, taskExecutor); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 长时间范围详细分析活跃仓库 |
||||
|
// */ |
||||
|
// private CompletableFuture<DetailedAnalysisResult> analyzeLongTermActiveRepositories(List<GiteaRepository> activeRepos, |
||||
|
// ZonedDateTime sinceTime, |
||||
|
// ZonedDateTime untilTime, |
||||
|
// String taskId) { |
||||
|
// return CompletableFuture.supplyAsync(() -> { |
||||
|
// Map<String, DeveloperData> devDataMap = new ConcurrentHashMap<>(); |
||||
|
// Map<String, RepoData> repoDataMap = new ConcurrentHashMap<>(); |
||||
|
// Map<DayOfWeek, Integer> dayStats = new ConcurrentHashMap<>(); |
||||
|
// Map<Integer, Integer> hourStats = new ConcurrentHashMap<>(); |
||||
|
// Map<String, Integer> fileTypeStats = new ConcurrentHashMap<>(); |
||||
|
// AtomicInteger totalCommits = new AtomicInteger(0); |
||||
|
// AtomicInteger processed = new AtomicInteger(0); |
||||
|
// |
||||
|
// List<CompletableFuture<Void>> futures = activeRepos.stream() |
||||
|
// .map(repo -> CompletableFuture.runAsync(() -> { |
||||
|
// try { |
||||
|
// analyzeLongTermSingleRepository(repo, sinceTime, untilTime, |
||||
|
// devDataMap, repoDataMap, dayStats, hourStats, fileTypeStats, totalCommits); |
||||
|
// } catch (Exception e) { |
||||
|
// log.debug("长时间任务[{}] 仓库 {} 详细分析失败: {}", taskId, repo.getFullPath(), e.getMessage()); |
||||
|
// } finally { |
||||
|
// int done = processed.incrementAndGet(); |
||||
|
// if (done % 5 == 0 || done == activeRepos.size()) { |
||||
|
// log.debug("长时间任务[{}] 详细分析进度: {}/{} | 提交: {}", taskId, done, activeRepos.size(), totalCommits.get()); |
||||
|
// } |
||||
|
// } |
||||
|
// }, taskExecutor)) |
||||
|
// .collect(Collectors.toList()); |
||||
|
// |
||||
|
// CompletableFuture<Void> allFutures = CompletableFuture.allOf( |
||||
|
// futures.toArray(new CompletableFuture[0])); |
||||
|
// |
||||
|
// try { |
||||
|
// allFutures.get(30, TimeUnit.SECONDS); |
||||
|
// } catch (Exception e) { |
||||
|
// log.warn("长时间任务[{}] 部分仓库详细分析未完成: {}", taskId, e.getMessage()); |
||||
|
// } |
||||
|
// |
||||
|
// return new DetailedAnalysisResult(devDataMap, repoDataMap, dayStats, hourStats, fileTypeStats, totalCommits.get()); |
||||
|
// }, taskExecutor); |
||||
|
// } |
||||
|
// |
||||
|
// // ==================== 核心方法(长时间范围优化版)==================== |
||||
|
// |
||||
|
// /** |
||||
|
// * 快速检查是否有提交(长时间范围优化版) |
||||
|
// * 只检查最近一段时间,避免全量查询 |
||||
|
// */ |
||||
|
// private boolean hasCommitsInLongTermRange(String repoFullName, ZonedDateTime since, ZonedDateTime until) throws Exception { |
||||
|
// // 策略:检查最近6个月或整个时间段的1/4,取较小值 |
||||
|
// Duration fullDuration = Duration.between(since, until); |
||||
|
// Duration checkDuration = Duration.ofDays(180); // 6个月 |
||||
|
// |
||||
|
// if (fullDuration.toDays() < 180) { |
||||
|
// // 如果总时间小于6个月,检查最后1/4的时间段 |
||||
|
// checkDuration = fullDuration.dividedBy(4); |
||||
|
// } |
||||
|
// |
||||
|
// ZonedDateTime checkStart = until.minus(checkDuration); |
||||
|
// if (checkStart.isBefore(since)) { |
||||
|
// checkStart = since; |
||||
|
// } |
||||
|
// |
||||
|
// String sinceStr = checkStart.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
// String untilStr = until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
// |
||||
|
// String url = String.format("%s/api/v1/repos/%s/commits?since=%s&until=%s&limit=1", |
||||
|
// giteaBaseUrl, repoFullName, sinceStr, untilStr); |
||||
|
// |
||||
|
// HttpRequest request = HttpRequest.newBuilder() |
||||
|
// .uri(java.net.URI.create(url)) |
||||
|
// .header("Authorization", "token " + accessToken) |
||||
|
// .timeout(Duration.ofSeconds(8)) |
||||
|
// .GET() |
||||
|
// .build(); |
||||
|
// |
||||
|
// HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); |
||||
|
// |
||||
|
// if (response.statusCode() != 200) { |
||||
|
// return false; |
||||
|
// } |
||||
|
// |
||||
|
// List<GiteaCommit> commits = objectMapper.readValue( |
||||
|
// response.body(), |
||||
|
// objectMapper.getTypeFactory().constructCollectionType(List.class, GiteaCommit.class)); |
||||
|
// |
||||
|
// return !commits.isEmpty(); |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 获取长时间范围内的提交(完整获取,无3个月限制) |
||||
|
// */ |
||||
|
// private List<GiteaCommit> getLongTermCommitsInRange(String repoFullName, ZonedDateTime since, ZonedDateTime until) throws Exception { |
||||
|
// String sinceStr = since.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
// String untilStr = until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
// |
||||
|
// String baseUrl = String.format("%s/api/v1/repos/%s/commits?since=%s&until=%s", |
||||
|
// giteaBaseUrl, repoFullName, sinceStr, untilStr); |
||||
|
// |
||||
|
// // 使用配置的超时时间 |
||||
|
// List<GiteaCommit> commits = fetchWithPaginationLongTerm(baseUrl, GiteaCommit.class, commitTimeoutSeconds); |
||||
|
// |
||||
|
// // 数据量过大时警告 |
||||
|
// if (commits.size() > 1000) { |
||||
|
// log.warn("仓库 {} 提交数量过大: {},建议缩小时间范围", repoFullName, commits.size()); |
||||
|
// } |
||||
|
// |
||||
|
// return commits; |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 分析单个仓库(长时间范围版) |
||||
|
// */ |
||||
|
// private void analyzeLongTermSingleRepository(GiteaRepository repo, |
||||
|
// ZonedDateTime sinceTime, |
||||
|
// ZonedDateTime untilTime, |
||||
|
// Map<String, DeveloperData> devDataMap, |
||||
|
// Map<String, RepoData> repoDataMap, |
||||
|
// Map<DayOfWeek, Integer> dayStats, |
||||
|
// Map<Integer, Integer> hourStats, |
||||
|
// Map<String, Integer> fileTypeStats, |
||||
|
// AtomicInteger totalCommits) throws Exception { |
||||
|
// |
||||
|
// String repoFullName = repo.getFullPath(); |
||||
|
// |
||||
|
// // 使用长时间范围方法获取提交 |
||||
|
// List<GiteaCommit> commits = getLongTermCommitsInRange(repoFullName, sinceTime, untilTime); |
||||
|
// |
||||
|
// if (!commits.isEmpty()) { |
||||
|
// RepoData repoData = new RepoData(); |
||||
|
// repoData.repoName = repoFullName; |
||||
|
// repoData.displayName = repo.getRepoName(); |
||||
|
// |
||||
|
// for (GiteaCommit commit : commits) { |
||||
|
// totalCommits.incrementAndGet(); |
||||
|
// String author = getAuthorName(commit); |
||||
|
// |
||||
|
// DeveloperData devData = devDataMap.computeIfAbsent(author, k -> new DeveloperData(author)); |
||||
|
// devData.commitCount++; |
||||
|
// devData.repos.add(repoFullName); |
||||
|
// |
||||
|
// repoData.commitCount++; |
||||
|
// repoData.developers.add(author); |
||||
|
// |
||||
|
// ZonedDateTime commitTime = commit.getCommitTime(); |
||||
|
// if (commitTime != null) { |
||||
|
// DayOfWeek day = commitTime.getDayOfWeek(); |
||||
|
// int hour = commitTime.getHour(); |
||||
|
// dayStats.merge(day, 1, Integer::sum); |
||||
|
// hourStats.merge(hour, 1, Integer::sum); |
||||
|
// } |
||||
|
// |
||||
|
// if (commit.getFiles() != null) { |
||||
|
// for (GiteaCommit.ChangedFile file : commit.getFiles()) { |
||||
|
// String fileType = file.getFileType(); |
||||
|
// fileTypeStats.merge(fileType, 1, Integer::sum); |
||||
|
// } |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// repoDataMap.put(repoFullName, repoData); |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// // ==================== 辅助方法 ==================== |
||||
|
// |
||||
|
// private List<GiteaRepository> getAllUserRepositories() throws Exception { |
||||
|
// String baseUrl = giteaBaseUrl + "/api/v1/user/repos?limit=50"; |
||||
|
// return fetchWithPaginationLongTerm(baseUrl, GiteaRepository.class, 10); |
||||
|
// } |
||||
|
// |
||||
|
// private <T> List<T> fetchWithPaginationLongTerm(String baseUrl, Class<T> clazz, int timeoutSeconds) throws Exception { |
||||
|
// List<T> results = new ArrayList<>(); |
||||
|
// int page = 1; |
||||
|
// |
||||
|
// while (page <= maxPages) { |
||||
|
// String pageUrl = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "page=" + page; |
||||
|
// |
||||
|
// HttpRequest request = HttpRequest.newBuilder() |
||||
|
// .uri(java.net.URI.create(pageUrl)) |
||||
|
// .header("Authorization", "token " + accessToken) |
||||
|
// .timeout(Duration.ofSeconds(timeoutSeconds)) |
||||
|
// .GET() |
||||
|
// .build(); |
||||
|
// |
||||
|
// HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); |
||||
|
// |
||||
|
// if (response.statusCode() != 200) { |
||||
|
// log.warn("长时间分析API请求失败: {} - {}", response.statusCode(), response.body()); |
||||
|
// break; |
||||
|
// } |
||||
|
// |
||||
|
// List<T> pageResults = objectMapper.readValue( |
||||
|
// response.body(), |
||||
|
// objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); |
||||
|
// |
||||
|
// if (pageResults.isEmpty()) { |
||||
|
// break; |
||||
|
// } |
||||
|
// |
||||
|
// results.addAll(pageResults); |
||||
|
// |
||||
|
// if (pageResults.size() < 50) { |
||||
|
// break; |
||||
|
// } |
||||
|
// |
||||
|
// page++; |
||||
|
// } |
||||
|
// |
||||
|
// return results; |
||||
|
// } |
||||
|
// |
||||
|
// private String getAuthorName(GiteaCommit commit) { |
||||
|
// if (commit.getCommit() != null && |
||||
|
// commit.getCommit().getAuthor() != null && |
||||
|
// commit.getCommit().getAuthor().getName() != null) { |
||||
|
// return commit.getCommit().getAuthor().getName(); |
||||
|
// } |
||||
|
// return "未知作者"; |
||||
|
// } |
||||
|
// |
||||
|
// // ==================== GitAnalysisData构建方法 ==================== |
||||
|
// |
||||
|
// /** |
||||
|
// * 构建完整的GitAnalysisData |
||||
|
// */ |
||||
|
// private GitAnalysisData buildGitAnalysisData(String since, String until, int totalRepos, int activeRepos, |
||||
|
// GiteaAnalysisService.DetailedAnalysisResult detailResult, long analysisTime) { |
||||
|
// GitAnalysisData data = new GitAnalysisData(); |
||||
|
// |
||||
|
// // 基础信息 |
||||
|
// data.setBasicInfo(new BasicInfo( |
||||
|
// since + " 至 " + until, |
||||
|
// totalRepos, |
||||
|
// activeRepos, |
||||
|
// detailResult.getDevDataMap().size(), |
||||
|
// detailResult.getTotalCommits(), |
||||
|
// analysisTime, |
||||
|
// "快速筛选 + 详细分析" |
||||
|
// )); |
||||
|
// |
||||
|
// // 开发者排行榜 |
||||
|
// if (!detailResult.getDevDataMap().isEmpty()) { |
||||
|
// List<GiteaAnalysisService.DeveloperData> devList = new ArrayList<>(detailResult.getDevDataMap().values()); |
||||
|
// devList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
// |
||||
|
// List<DeveloperRank> developerRanks = new ArrayList<>(); |
||||
|
// int rank = 1; |
||||
|
// for (GiteaAnalysisService.DeveloperData dev : devList) { |
||||
|
// if (rank > 10) break; |
||||
|
// developerRanks.add(new DeveloperRank(rank++, dev.name, dev.commitCount, dev.repos.size())); |
||||
|
// } |
||||
|
// data.setDeveloperRanks(developerRanks); |
||||
|
// } |
||||
|
// |
||||
|
// // 仓库排行榜 |
||||
|
// if (!detailResult.getRepoDataMap().isEmpty()) { |
||||
|
// List<GiteaAnalysisService.RepoData> repoList = new ArrayList<>(detailResult.getRepoDataMap().values()); |
||||
|
// repoList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
// |
||||
|
// List<RepoRank> repoRanks = new ArrayList<>(); |
||||
|
// int rank = 1; |
||||
|
// for (GiteaAnalysisService.RepoData repo : repoList) { |
||||
|
// if (rank > 10) break; |
||||
|
// repoRanks.add(new RepoRank(rank++, repo.repoName, repo.displayName, repo.commitCount, repo.developers.size())); |
||||
|
// } |
||||
|
// data.setRepoRanks(repoRanks); |
||||
|
// } |
||||
|
// |
||||
|
// // 时间分布(按星期) |
||||
|
// if (!detailResult.getDayStats().isEmpty()) { |
||||
|
// String[] dayNames = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; |
||||
|
// DayOfWeek[] days = DayOfWeek.values(); |
||||
|
// |
||||
|
// List<DayStats> dayStatsList = new ArrayList<>(); |
||||
|
// for (int i = 0; i < 7; i++) { |
||||
|
// int count = detailResult.getDayStats().getOrDefault(days[i], 0); |
||||
|
// dayStatsList.add(new DayStats(dayNames[i], count)); |
||||
|
// } |
||||
|
// data.setDayStats(dayStatsList); |
||||
|
// } |
||||
|
// |
||||
|
// // 文件类型统计 |
||||
|
// if (!detailResult.getFileTypeStats().isEmpty()) { |
||||
|
// List<Map.Entry<String, Integer>> fileList = new ArrayList<>(detailResult.getFileTypeStats().entrySet()); |
||||
|
// fileList.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); |
||||
|
// |
||||
|
// List<FileTypeStats> fileTypeStatsList = new ArrayList<>(); |
||||
|
// for (Map.Entry<String, Integer> entry : fileList) { |
||||
|
// fileTypeStatsList.add(new FileTypeStats(entry.getKey(), entry.getValue())); |
||||
|
// } |
||||
|
// data.setFileTypeStats(fileTypeStatsList); |
||||
|
// } |
||||
|
// |
||||
|
// // 生成时间 |
||||
|
// data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
// |
||||
|
// return data; |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 构建空的GitAnalysisData(无仓库时) |
||||
|
// */ |
||||
|
// private GitAnalysisData buildEmptyGitAnalysisData(String message) { |
||||
|
// GitAnalysisData data = new GitAnalysisData(); |
||||
|
// data.setBasicInfo(new BasicInfo( |
||||
|
// "无时间范围", |
||||
|
// 0, |
||||
|
// 0, |
||||
|
// 0, |
||||
|
// 0, |
||||
|
// 0, |
||||
|
// "无仓库数据" |
||||
|
// )); |
||||
|
// data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
// return data; |
||||
|
// } |
||||
|
// |
||||
|
// /** |
||||
|
// * 构建简单的GitAnalysisData(无活跃仓库时) |
||||
|
// */ |
||||
|
// private GitAnalysisData buildSimpleGitAnalysisData(String since, String until, int totalRepos, |
||||
|
// int activeRepos, int totalDevs, long startTime) { |
||||
|
// long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
// |
||||
|
// GitAnalysisData data = new GitAnalysisData(); |
||||
|
// data.setBasicInfo(new BasicInfo( |
||||
|
// since + " 至 " + until, |
||||
|
// totalRepos, |
||||
|
// activeRepos, |
||||
|
// totalDevs, |
||||
|
// 0, |
||||
|
// analysisTime, |
||||
|
// "快速筛选" |
||||
|
// )); |
||||
|
// data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
||||
|
// |
||||
|
// return data; |
||||
|
// } |
||||
|
// |
||||
|
// // ==================== 内部数据类 ==================== |
||||
|
// |
||||
|
// private static class DeveloperData { |
||||
|
// String name; |
||||
|
// int commitCount = 0; |
||||
|
// Set<String> repos = new HashSet<>(); |
||||
|
// |
||||
|
// DeveloperData(String name) { |
||||
|
// this.name = name; |
||||
|
// } |
||||
|
// } |
||||
|
// |
||||
|
// private static class RepoData { |
||||
|
// String repoName; |
||||
|
// String displayName; |
||||
|
// int commitCount = 0; |
||||
|
// Set<String> developers = new HashSet<>(); |
||||
|
// } |
||||
|
// |
||||
|
// // ==================== 详细分析结果类 ==================== |
||||
|
// |
||||
|
// private static class DetailedAnalysisResult { |
||||
|
// private final Map<String, GiteaAnalysisService.DeveloperData> devDataMap; |
||||
|
// private final Map<String, GiteaAnalysisService.RepoData> repoDataMap; |
||||
|
// private final Map<DayOfWeek, Integer> dayStats; |
||||
|
// private final Map<Integer, Integer> hourStats; |
||||
|
// private final Map<String, Integer> fileTypeStats; |
||||
|
// private final int totalCommits; |
||||
|
// |
||||
|
// public DetailedAnalysisResult(Map<String, GiteaAnalysisService.DeveloperData> devDataMap, Map<String, GiteaAnalysisService.RepoData> repoDataMap, |
||||
|
// Map<DayOfWeek, Integer> dayStats, Map<Integer, Integer> hourStats, |
||||
|
// Map<String, Integer> fileTypeStats, int totalCommits) { |
||||
|
// this.devDataMap = devDataMap; |
||||
|
// this.repoDataMap = repoDataMap; |
||||
|
// this.dayStats = dayStats; |
||||
|
// this.hourStats = hourStats; |
||||
|
// this.fileTypeStats = fileTypeStats; |
||||
|
// this.totalCommits = totalCommits; |
||||
|
// } |
||||
|
// |
||||
|
// public Map<String, GiteaAnalysisService.DeveloperData> getDevDataMap() { return devDataMap; } |
||||
|
// public Map<String, GiteaAnalysisService.RepoData> getRepoDataMap() { return repoDataMap; } |
||||
|
// public Map<DayOfWeek, Integer> getDayStats() { return dayStats; } |
||||
|
// public Map<Integer, Integer> getHourStats() { return hourStats; } |
||||
|
// public Map<String, Integer> getFileTypeStats() { return fileTypeStats; } |
||||
|
// public int getTotalCommits() { return totalCommits; } |
||||
|
// } |
||||
|
//} |
||||
@ -0,0 +1,208 @@ |
|||||
|
package com.chenhai.chenhaiai.service; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.vladsch.flexmark.html.HtmlRenderer; |
||||
|
import com.vladsch.flexmark.parser.Parser; |
||||
|
import com.vladsch.flexmark.util.ast.Node; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Service |
||||
|
public class MarkdownService { |
||||
|
|
||||
|
private final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
/** |
||||
|
* 查询结果原样全部转换为Markdown,不筛选不截断 |
||||
|
*/ |
||||
|
public String fullJsonToMarkdown(String jsonStr) throws Exception { |
||||
|
Map<String, Object> data = objectMapper.readValue(jsonStr, Map.class); |
||||
|
StringBuilder md = new StringBuilder(); |
||||
|
|
||||
|
// 1. 基本信息原样转换 |
||||
|
md.append("# ").append(getValue(data, "deptName")).append("\n\n"); |
||||
|
md.append("## ").append(getValue(data, "weekDisplay")).append("\n\n"); |
||||
|
|
||||
|
// 2. 部门信息 |
||||
|
Object deptObj = data.get("dept"); |
||||
|
if (deptObj instanceof Map) { |
||||
|
md.append("**部门**:").append(getValue((Map)deptObj, "deptName")).append("\n\n"); |
||||
|
} |
||||
|
|
||||
|
// 3. 计划任务 - 全部转换 |
||||
|
md.append("## 计划任务\n\n"); |
||||
|
Object planDetails = data.get("planDetails"); |
||||
|
if (planDetails instanceof List) { |
||||
|
List<Map<String, Object>> plans = (List<Map<String, Object>>) planDetails; |
||||
|
md.append("**总数**:").append(plans.size()).append(" 项\n\n"); |
||||
|
|
||||
|
md.append("| 项目 | 任务内容 | 负责人 | 完成状态 | 备注 |\n"); |
||||
|
md.append("|------|----------|--------|----------|------|\n"); |
||||
|
|
||||
|
for (Map<String, Object> plan : plans) { |
||||
|
md.append(String.format("| %s | %s | %s | %s | %s |\n", |
||||
|
getValue(plan, "projectName"), |
||||
|
getValue(plan, "content"), |
||||
|
getValue(plan, "developer"), |
||||
|
getValue(plan, "superviseStatus"), |
||||
|
getValue(plan, "note") |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
|
||||
|
// 4. 工作日报 - 全部转换 |
||||
|
md.append("## 工作日报\n\n"); |
||||
|
Object dailyPapers = data.get("dailyPapers"); |
||||
|
if (dailyPapers instanceof List) { |
||||
|
List<Map<String, Object>> dailies = (List<Map<String, Object>>) dailyPapers; |
||||
|
md.append("**总数**:").append(dailies.size()).append(" 条\n\n"); |
||||
|
|
||||
|
md.append("| 日期 | 项目 | 工作内容 | 工时 |\n"); |
||||
|
md.append("|------|------|----------|------|\n"); |
||||
|
|
||||
|
for (Map<String, Object> daily : dailies) { |
||||
|
md.append(String.format("| %s | %s | %s | %s |\n", |
||||
|
getValue(daily, "dailyPaperDate"), |
||||
|
getValue(daily, "projectName"), |
||||
|
getValue(daily, "content"), |
||||
|
getValue(daily, "dailyPaperHour") |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
|
||||
|
// 5. 成员信息 - 全部转换 |
||||
|
md.append("## 团队成员\n\n"); |
||||
|
Object userInfos = data.get("userInfos"); |
||||
|
if (userInfos instanceof List) { |
||||
|
List<Map<String, Object>> users = (List<Map<String, Object>>) userInfos; |
||||
|
|
||||
|
md.append("| 用户ID | 姓名 |\n"); |
||||
|
md.append("|--------|------|\n"); |
||||
|
|
||||
|
for (Map<String, Object> user : users) { |
||||
|
md.append(String.format("| %s | %s |\n", |
||||
|
getValue(user, "userId"), |
||||
|
getValue(user, "userName") |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
|
||||
|
// 6. Git数据 - 全部转换(如果有) |
||||
|
md.append("## Git提交分析\n\n"); |
||||
|
Object gitAnalysis = data.get("gitAnalysis"); |
||||
|
if (gitAnalysis instanceof Map) { |
||||
|
Map<String, Object> git = (Map<String, Object>) gitAnalysis; |
||||
|
|
||||
|
// 基础信息 |
||||
|
Object basicInfo = git.get("basicInfo"); |
||||
|
if (basicInfo instanceof Map) { |
||||
|
Map<String, Object> basic = (Map<String, Object>) basicInfo; |
||||
|
md.append("### 基础信息\n\n"); |
||||
|
md.append("| 项目 | 值 |\n"); |
||||
|
md.append("|------|----|\n"); |
||||
|
for (Map.Entry<String, Object> entry : basic.entrySet()) { |
||||
|
md.append(String.format("| %s | %s |\n", |
||||
|
entry.getKey(), |
||||
|
entry.getValue() |
||||
|
)); |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
} |
||||
|
|
||||
|
// 成员排名 |
||||
|
Object devRanks = git.get("developerRanks"); |
||||
|
if (devRanks instanceof List) { |
||||
|
md.append("### 成员提交排名\n\n"); |
||||
|
md.append("| 排名 | 成员 | 提交次数 | 参与项目数 |\n"); |
||||
|
md.append("|------|------|----------|------------|\n"); |
||||
|
|
||||
|
List<Map<String, Object>> ranks = (List<Map<String, Object>>) devRanks; |
||||
|
for (Map<String, Object> rank : ranks) { |
||||
|
md.append(String.format("| %s | %s | %s | %s |\n", |
||||
|
getValue(rank, "rank"), |
||||
|
getValue(rank, "name"), |
||||
|
getValue(rank, "commitCount"), |
||||
|
getValue(rank, "repoCount") |
||||
|
)); |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
} |
||||
|
|
||||
|
// 仓库排名 |
||||
|
Object repoRanks = git.get("repoRanks"); |
||||
|
if (repoRanks instanceof List) { |
||||
|
md.append("### 仓库活跃度排名\n\n"); |
||||
|
md.append("| 排名 | 仓库 | 提交次数 | 开发者数 |\n"); |
||||
|
md.append("|------|------|----------|----------|\n"); |
||||
|
|
||||
|
List<Map<String, Object>> repos = (List<Map<String, Object>>) repoRanks; |
||||
|
for (Map<String, Object> repo : repos) { |
||||
|
md.append(String.format("| %s | %s | %s | %s |\n", |
||||
|
getValue(repo, "rank"), |
||||
|
getValue(repo, "displayName"), |
||||
|
getValue(repo, "commitCount"), |
||||
|
getValue(repo, "developerCount") |
||||
|
)); |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
} |
||||
|
|
||||
|
// 每日统计 |
||||
|
Object dayStats = git.get("dayStats"); |
||||
|
if (dayStats instanceof List) { |
||||
|
md.append("### 每日提交统计\n\n"); |
||||
|
md.append("| 星期 | 提交次数 |\n"); |
||||
|
md.append("|------|----------|\n"); |
||||
|
|
||||
|
List<Map<String, Object>> days = (List<Map<String, Object>>) dayStats; |
||||
|
for (Map<String, Object> day : days) { |
||||
|
md.append(String.format("| %s | %s |\n", |
||||
|
getValue(day, "dayName"), |
||||
|
getValue(day, "commitCount") |
||||
|
)); |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
} |
||||
|
} else { |
||||
|
md.append("无Git数据\n\n"); |
||||
|
} |
||||
|
|
||||
|
// 7. 其他字段原样显示 |
||||
|
md.append("## 其他信息\n\n"); |
||||
|
|
||||
|
// 周计划主信息 |
||||
|
Object planMain = data.get("planMain"); |
||||
|
if (planMain instanceof Map) { |
||||
|
md.append("### 周计划主信息\n\n"); |
||||
|
Map<String, Object> main = (Map<String, Object>) planMain; |
||||
|
for (Map.Entry<String, Object> entry : main.entrySet()) { |
||||
|
md.append("- **").append(entry.getKey()).append("**: ").append(entry.getValue()).append("\n"); |
||||
|
} |
||||
|
md.append("\n"); |
||||
|
} |
||||
|
|
||||
|
// 是否是研发部门 |
||||
|
Object isResearch = data.get("isResearchDept"); |
||||
|
if (isResearch != null) { |
||||
|
md.append("**是否是研发部门**: ").append(isResearch).append("\n\n"); |
||||
|
} |
||||
|
|
||||
|
return md.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 安全获取值,只转义特殊字符 |
||||
|
*/ |
||||
|
private String getValue(Map<String, Object> map, String key) { |
||||
|
Object value = map.get(key); |
||||
|
if (value == null) return ""; |
||||
|
|
||||
|
String str = value.toString(); |
||||
|
// 只转义表格分隔符,保持其他原样 |
||||
|
return str.replace("|", "\\|").replace("\n", " "); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
package com.chenhai.chenhaiai.service; |
||||
|
|
||||
|
import java.time.ZonedDateTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.concurrent.*; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
|
||||
|
public class SimpleConcurrentTest { |
||||
|
|
||||
|
// 模拟的Gitea分析服务 |
||||
|
static class MockGiteaAnalysisService { |
||||
|
private final ExecutorService executor; |
||||
|
private final AtomicInteger activeTasks = new AtomicInteger(0); |
||||
|
|
||||
|
public MockGiteaAnalysisService(int poolSize) { |
||||
|
this.executor = Executors.newFixedThreadPool(poolSize); |
||||
|
System.out.println("初始化线程池,大小: " + poolSize); |
||||
|
} |
||||
|
|
||||
|
public CompletableFuture<String> performAnalysisAsync(String since, String until) { |
||||
|
activeTasks.incrementAndGet(); |
||||
|
String taskId = "Task-" + System.currentTimeMillis(); |
||||
|
|
||||
|
System.out.printf(" [%s] 开始处理: %s 至 %s\n", |
||||
|
taskId, since.substring(0, 10), until.substring(0, 10)); |
||||
|
|
||||
|
return CompletableFuture.supplyAsync(() -> { |
||||
|
try { |
||||
|
// 模拟耗时操作(1分钟) |
||||
|
Thread.sleep(60000); |
||||
|
|
||||
|
// 模拟结果 |
||||
|
String result = String.format("分析报告: %s - %s (仓库: 15个, 提交: 230次)", |
||||
|
since.substring(0, 10), until.substring(0, 10)); |
||||
|
|
||||
|
return result; |
||||
|
|
||||
|
} catch (InterruptedException e) { |
||||
|
throw new RuntimeException("任务被中断"); |
||||
|
} finally { |
||||
|
activeTasks.decrementAndGet(); |
||||
|
} |
||||
|
}, executor); |
||||
|
} |
||||
|
|
||||
|
public int getActiveTaskCount() { |
||||
|
return activeTasks.get(); |
||||
|
} |
||||
|
|
||||
|
public void shutdown() { |
||||
|
executor.shutdown(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
System.out.println("🚀 开始并发模拟测试"); |
||||
|
System.out.println("模拟场景: 5个用户同时请求Git分析"); |
||||
|
System.out.println("每个分析任务耗时: 60秒\n"); |
||||
|
|
||||
|
MockGiteaAnalysisService service = new MockGiteaAnalysisService(8); |
||||
|
|
||||
|
try { |
||||
|
// 测试数据 |
||||
|
String since = getLastMonday(); |
||||
|
String until = getLastSunday(); |
||||
|
|
||||
|
// 并发测试 |
||||
|
testConcurrentRequests(service, since, until); |
||||
|
|
||||
|
} finally { |
||||
|
service.shutdown(); |
||||
|
System.out.println("\n✅ 测试完成"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void testConcurrentRequests(MockGiteaAnalysisService service, String since, String until) { |
||||
|
List<CompletableFuture<String>> futures = new ArrayList<>(); |
||||
|
CountDownLatch startLatch = new CountDownLatch(1); |
||||
|
AtomicInteger completed = new AtomicInteger(0); |
||||
|
|
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
|
||||
|
// 创建5个并发任务 |
||||
|
for (int i = 1; i <= 5; i++) { |
||||
|
final int userId = i; |
||||
|
|
||||
|
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { |
||||
|
try { |
||||
|
// 等待统一开始 |
||||
|
startLatch.await(); |
||||
|
|
||||
|
System.out.printf("👤 用户%d: 开始分析...\n", userId); |
||||
|
|
||||
|
return service.performAnalysisAsync(since, until).get(); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.printf("用户%d失败: %s\n", userId, e.getMessage()); |
||||
|
return null; |
||||
|
} |
||||
|
}).thenApply(result -> { |
||||
|
int done = completed.incrementAndGet(); |
||||
|
long elapsed = (System.currentTimeMillis() - startTime) / 1000; |
||||
|
|
||||
|
if (result != null) { |
||||
|
System.out.printf("✅ 用户%d完成! (耗时: %ds, 进度: %d/5)\n", |
||||
|
userId, elapsed, done); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
}); |
||||
|
|
||||
|
futures.add(future); |
||||
|
} |
||||
|
|
||||
|
// 统一开始所有任务 |
||||
|
System.out.println("\n📢 所有用户准备就绪,3秒后同时开始..."); |
||||
|
sleep(3000); |
||||
|
|
||||
|
System.out.println("🎬 开始!"); |
||||
|
startLatch.countDown(); |
||||
|
|
||||
|
// 等待所有完成 |
||||
|
try { |
||||
|
CompletableFuture<Void> allDone = CompletableFuture.allOf( |
||||
|
futures.toArray(new CompletableFuture[0])); |
||||
|
|
||||
|
allDone.get(65, TimeUnit.SECONDS); // 超时65秒 |
||||
|
|
||||
|
long totalTime = System.currentTimeMillis() - startTime; |
||||
|
System.out.printf("\n🎉 全部完成! 总耗时: %.1f秒\n", totalTime / 1000.0); |
||||
|
|
||||
|
// 显示并发效果 |
||||
|
System.out.println("\n⚡ 并发效果分析:"); |
||||
|
System.out.println("串行处理: 5任务 × 60秒 = 300秒 (5分钟)"); |
||||
|
System.out.printf("并发处理: %.1f秒\n", totalTime / 1000.0); |
||||
|
System.out.printf("性能提升: %.1f倍\n", 300.0 / (totalTime / 1000.0)); |
||||
|
|
||||
|
} catch (TimeoutException e) { |
||||
|
System.err.println("⏰ 测试超时"); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("测试异常: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static String getLastMonday() { |
||||
|
ZonedDateTime time = ZonedDateTime.now() |
||||
|
.minusWeeks(1) |
||||
|
.with(java.time.DayOfWeek.MONDAY) |
||||
|
.withHour(0).withMinute(0).withSecond(0); |
||||
|
|
||||
|
return time.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
} |
||||
|
|
||||
|
private static String getLastSunday() { |
||||
|
ZonedDateTime time = ZonedDateTime.now() |
||||
|
.minusWeeks(1) |
||||
|
.with(java.time.DayOfWeek.SUNDAY) |
||||
|
.withHour(23).withMinute(59).withSecond(59); |
||||
|
|
||||
|
return time.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
} |
||||
|
|
||||
|
private static void sleep(long millis) { |
||||
|
try { |
||||
|
Thread.sleep(millis); |
||||
|
} catch (InterruptedException e) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,907 @@ |
|||||
|
package com.chenhai.chenhaiai.service.gitNew; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.*; |
||||
|
import com.chenhai.common.core.redis.RedisCache; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.net.http.HttpClient; |
||||
|
import java.net.http.HttpRequest; |
||||
|
import java.net.http.HttpResponse; |
||||
|
import java.time.Duration; |
||||
|
import java.time.LocalDate; |
||||
|
import java.time.ZonedDateTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.*; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
/** |
||||
|
* Gitea数据同步服务 |
||||
|
* |
||||
|
* <p>本服务负责从Gitea代码仓库平台同步数据到本地Redis缓存,提供全量同步和增量同步两种模式, |
||||
|
* 支持多仓库批量处理,用于代码提交分析、开发行为统计等场景。</p> |
||||
|
* |
||||
|
* <h3>核心功能:</h3> |
||||
|
* <ol> |
||||
|
* <li><b>全量同步</b>:拉取仓库所有历史提交,适用于首次数据初始化</li> |
||||
|
* <li><b>增量同步</b>:基于最后同步时间,只获取新增提交,适用于日常数据更新</li> |
||||
|
* <li><b>智能时间管理</b>:自动记录和管理同步时间点,形成连续同步链条</li> |
||||
|
* <li><b>分页数据获取</b>:处理Gitea API分页,支持大仓库数据完整获取</li> |
||||
|
* <li><b>Redis缓存管理</b>:结构化存储提交数据,支持多维度索引查询</li> |
||||
|
* </ol> |
||||
|
* |
||||
|
* <h3>数据存储结构:</h3> |
||||
|
* <pre> |
||||
|
* Redis Key 结构: |
||||
|
* 1. 仓库列表:gitea:repos:list -> Map<仓库ID, 仓库路径> |
||||
|
* 2. 仓库信息:gitea:repo:{fullPath} -> Map<字段名, 值> |
||||
|
* 3. 提交详情:gitea:commit:{fullPath}:{sha} -> Map<字段名, 值> |
||||
|
* 4. 日期索引:gitea:commits:by_date:{date} -> Set<提交SHA> |
||||
|
* 5. 仓库提交索引:gitea:repo_commits:{fullPath} -> Set<提交SHA> |
||||
|
* 6. 同步进度:gitea:sync:progress -> Map<进度信息> |
||||
|
* 7. 最后同步时间:gitea:last_sync:{fullPath} -> 时间戳(新增) |
||||
|
* </pre> |
||||
|
* |
||||
|
* <h3>使用场景案例:</h3> |
||||
|
* |
||||
|
* <h4>案例1:首次系统部署(全量初始化)</h4> |
||||
|
* <pre> |
||||
|
* // 1. 获取所有仓库 |
||||
|
* List<GiteaRepository> repos = getAllRepositories(); |
||||
|
* |
||||
|
* // 2. 全量同步所有仓库(建议夜间执行) |
||||
|
* syncAllReposAllCommits(); |
||||
|
* |
||||
|
* // 3. 查看进度 |
||||
|
* Map<String, Object> progress = getSyncProgress(); |
||||
|
* </pre> |
||||
|
* |
||||
|
* <h4>案例2:日常数据更新(增量同步)</h4> |
||||
|
* <pre> |
||||
|
* // 1. 定时任务调用(如每10分钟) |
||||
|
* syncIncrementalCommitsForRepo("ChenHaiTech/project1"); |
||||
|
* |
||||
|
* // 2. 或批量增量同步所有仓库 |
||||
|
* // (可外部定时调用,不需要内部定时任务) |
||||
|
* syncIncrementalForAllRepos(); // 如果有此方法 |
||||
|
* </pre> |
||||
|
* |
||||
|
* <h4>案例3:新增仓库处理</h4> |
||||
|
* <pre> |
||||
|
* // 1. 获取最新仓库列表 |
||||
|
* List<GiteaRepository> repos = getAllRepositories(); |
||||
|
* |
||||
|
* // 2. 对新仓库进行全量同步 |
||||
|
* syncAllCommitsForRepo("ChenHaiTech/new-project"); |
||||
|
* |
||||
|
* // 3. 后续自动进入增量同步模式 |
||||
|
* </pre> |
||||
|
* |
||||
|
* <h3>核心方法说明:</h3> |
||||
|
* <table border="1"> |
||||
|
* <tr> |
||||
|
* <th>方法名</th> |
||||
|
* <th>作用</th> |
||||
|
* <th>适用场景</th> |
||||
|
* <th>耗时预估</th> |
||||
|
* </tr> |
||||
|
* <tr> |
||||
|
* <td>getAllRepositories()</td> |
||||
|
* <td>获取用户所有仓库列表</td> |
||||
|
* <td>首次获取、定期更新仓库列表</td> |
||||
|
* <td>1-5秒</td> |
||||
|
* </tr> |
||||
|
* <tr> |
||||
|
* <td>syncAllCommitsForRepo()</td> |
||||
|
* <td>同步单个仓库所有历史提交</td> |
||||
|
* <td>新仓库初始化、数据修复</td> |
||||
|
* <td>依赖仓库大小</td> |
||||
|
* </tr> |
||||
|
* <tr> |
||||
|
* <td>syncIncrementalCommitsForRepo()</td> |
||||
|
* <td>增量同步单个仓库</td> |
||||
|
* <td>日常数据更新、定时任务</td> |
||||
|
* <td>几秒到几分钟</td> |
||||
|
* </tr> |
||||
|
* <tr> |
||||
|
* <td>syncAllReposAllCommits()</td> |
||||
|
* <td>批量全量同步所有仓库</td> |
||||
|
* <td>系统首次部署、月度全量更新</td> |
||||
|
* <td>几小时(183个仓库)</td> |
||||
|
* </tr> |
||||
|
* <tr> |
||||
|
* <td>getSyncProgress()</td> |
||||
|
* <td>获取批量同步进度</td> |
||||
|
* <td>监控长时间同步任务</td> |
||||
|
* <td>实时</td> |
||||
|
* </tr> |
||||
|
* </table> |
||||
|
* |
||||
|
* <h3>智能时间链机制:</h3> |
||||
|
* <p>增量同步采用智能时间判断,避免数据遗漏或重复:</p> |
||||
|
* <ul> |
||||
|
* <li><b>有记录</b>:从上次同步时间-10分钟开始(防止边界遗漏)</li> |
||||
|
* <li><b>无记录但有数据</b>:从24小时前开始(保守策略,保护已有全量数据)</li> |
||||
|
* <li><b>完全无数据</b>:从7天前开始(新仓库首次增量)</li> |
||||
|
* <li><b>时间保护</b>:最大查询范围30天,防止长时间查询</li> |
||||
|
* </ul> |
||||
|
* |
||||
|
* <h3>注意事项:</h3> |
||||
|
* <ol> |
||||
|
* <li>全量同步数据已存在时,增量同步会自动识别并采用保守策略</li> |
||||
|
* <li>Redis数据默认30天过期,长期不访问的数据会自动清理</li> |
||||
|
* <li>批量操作有间隔和限流,避免对Gitea服务器造成压力</li> |
||||
|
* <li>所有公开方法签名保持稳定,外部调用无需修改</li> |
||||
|
* <li>增量同步依赖最后同步时间记录,形成连续时间链</li> |
||||
|
* </ol> |
||||
|
* |
||||
|
* <h3>配置说明:</h3> |
||||
|
* <pre> |
||||
|
* # application.yml |
||||
|
* gitea: |
||||
|
* url: http://192.168.1.224:3000 # Gitea服务器地址 |
||||
|
* token: a9f1c8d3d6fefd73956604f496457faaa3672f89 # 访问令牌 |
||||
|
* </pre> |
||||
|
* |
||||
|
* <h3>版本更新说明:</h3> |
||||
|
* <p><b>当前版本主要改进:</b></p> |
||||
|
* <ul> |
||||
|
* <li>增量同步逻辑增强,基于最后同步时间智能判断</li> |
||||
|
* <li>新增时间链管理,避免数据遗漏</li> |
||||
|
* <li>保持所有外部接口兼容性,现有调用无需修改</li> |
||||
|
* <li>增强日志记录,便于问题排查</li> |
||||
|
* </ul> |
||||
|
* |
||||
|
* @author 系统自动生成 |
||||
|
* @since 2024-01 |
||||
|
* @version 2.0 (增强增量同步版本) |
||||
|
*/ |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class GiteaDataService { |
||||
|
|
||||
|
@Value("${gitea.url:http://chrdcenter.chenhaitech.com:29830}") |
||||
|
private String giteaBaseUrl; |
||||
|
|
||||
|
@Value("${gitea.token:a9f1c8d3d6fefd73956604f496457faaa3672f89}") |
||||
|
private String accessToken; |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisCache redisCache; |
||||
|
|
||||
|
private HttpClient httpClient; |
||||
|
private ObjectMapper objectMapper; |
||||
|
|
||||
|
// Redis Key常量 |
||||
|
private static final String REPO_LIST_KEY = "gitea:repos:list"; |
||||
|
private static final String REPO_INFO_PREFIX = "gitea:repo:"; |
||||
|
private static final String COMMIT_PREFIX = "gitea:commit:"; |
||||
|
private static final String COMMITS_BY_DATE_PREFIX = "gitea:commits:by_date:"; |
||||
|
private static final String REPO_COMMITS_INDEX_PREFIX = "gitea:repo_commits:"; |
||||
|
private static final String SYNC_PROGRESS_KEY = "gitea:sync:progress"; |
||||
|
|
||||
|
public GiteaDataService() { |
||||
|
this.httpClient = HttpClient.newBuilder() |
||||
|
.connectTimeout(Duration.ofSeconds(10)) |
||||
|
.build(); |
||||
|
|
||||
|
this.objectMapper = new ObjectMapper(); |
||||
|
this.objectMapper.registerModule(new JavaTimeModule()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 1. 获取所有仓库 |
||||
|
*/ |
||||
|
public List<GiteaRepository> getAllRepositories() throws Exception { |
||||
|
String baseUrl = giteaBaseUrl + "/api/v1/user/repos?limit=50"; |
||||
|
List<GiteaRepository> repos = fetchWithPagination(baseUrl, GiteaRepository.class, 30); |
||||
|
|
||||
|
// 存储仓库列表到Redis |
||||
|
if (!repos.isEmpty()) { |
||||
|
Map<String, String> repoMap = new LinkedHashMap<>(); |
||||
|
for (GiteaRepository repo : repos) { |
||||
|
repoMap.put(String.valueOf(repo.getId()), repo.getFullPath()); |
||||
|
|
||||
|
// 存储仓库基本信息 |
||||
|
String repoKey = REPO_INFO_PREFIX + repo.getFullPath(); |
||||
|
Map<String, Object> repoInfo = new HashMap<>(); |
||||
|
repoInfo.put("id", repo.getId()); |
||||
|
repoInfo.put("name", repo.getRepoName()); |
||||
|
repoInfo.put("fullPath", repo.getFullPath()); |
||||
|
|
||||
|
// createdAt 是 String 类型,直接存储 |
||||
|
repoInfo.put("createdAt", repo.getCreatedAt()); |
||||
|
|
||||
|
redisCache.setCacheMap(repoKey, repoInfo); |
||||
|
redisCache.expire(repoKey, 30, TimeUnit.DAYS); |
||||
|
} |
||||
|
|
||||
|
redisCache.setCacheMap(REPO_LIST_KEY, repoMap); |
||||
|
redisCache.expire(REPO_LIST_KEY, 30, TimeUnit.DAYS); |
||||
|
} |
||||
|
|
||||
|
return repos; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 2. 拉取单个仓库的所有历史提交(从创建至今) |
||||
|
*/ |
||||
|
public void syncAllCommitsForRepo(String repoFullPath) throws Exception { |
||||
|
log.info("开始同步仓库所有历史提交: {}", repoFullPath); |
||||
|
|
||||
|
// 使用最大limit,减少分页次数 |
||||
|
String baseUrl = String.format("%s/api/v1/repos/%s/commits?limit=100&stat=true", |
||||
|
giteaBaseUrl, repoFullPath); |
||||
|
|
||||
|
log.info("提交获取URL: {}", baseUrl); |
||||
|
|
||||
|
List<GiteaCommit> allCommits = fetchWithPagination(baseUrl, GiteaCommit.class, 300); // 5分钟超时 |
||||
|
|
||||
|
log.info("仓库 {} 共获取到 {} 个提交", repoFullPath, allCommits.size()); |
||||
|
|
||||
|
if (allCommits.isEmpty()) { |
||||
|
log.info("仓库 {} 无提交数据", repoFullPath); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 分批存储到Redis,避免内存和Redis压力 |
||||
|
int batchSize = 200; |
||||
|
int totalBatches = (int) Math.ceil((double) allCommits.size() / batchSize); |
||||
|
|
||||
|
log.info("开始分批存储,每批{}条,共{}批", batchSize, totalBatches); |
||||
|
|
||||
|
for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) { |
||||
|
int start = batchIndex * batchSize; |
||||
|
int end = Math.min(start + batchSize, allCommits.size()); |
||||
|
List<GiteaCommit> batch = allCommits.subList(start, end); |
||||
|
|
||||
|
int savedInBatch = 0; |
||||
|
for (GiteaCommit commit : batch) { |
||||
|
try { |
||||
|
storeCommit(repoFullPath, commit); |
||||
|
savedInBatch++; |
||||
|
} catch (Exception e) { |
||||
|
log.warn("存储提交失败(SHA:{}): {}", |
||||
|
commit.getSha().substring(0, 8), e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
log.info("批次 {}/{} 完成,存储 {}/{} 条", |
||||
|
batchIndex + 1, totalBatches, savedInBatch, batch.size()); |
||||
|
|
||||
|
// 批次间休息,减轻Redis压力 |
||||
|
if (batchIndex < totalBatches - 1) { |
||||
|
Thread.sleep(200); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
log.info("仓库 {} 提交数据同步完成,总计 {} 条", repoFullPath, allCommits.size()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 3. 存储单个提交到Redis |
||||
|
*/ |
||||
|
private void storeCommit(String repoFullPath, GiteaCommit commit) throws Exception { |
||||
|
String commitKey = COMMIT_PREFIX + repoFullPath + ":" + commit.getSha(); |
||||
|
|
||||
|
// 检查是否已存在 |
||||
|
if (redisCache.hasKey(commitKey)) { |
||||
|
return; // 已存在,跳过 |
||||
|
} |
||||
|
|
||||
|
Map<String, Object> commitData = new HashMap<>(); |
||||
|
commitData.put("sha", commit.getSha()); |
||||
|
commitData.put("author", getAuthorName(commit)); |
||||
|
commitData.put("message", commit.getCommit().getMessage()); |
||||
|
|
||||
|
// commitTime 也需要处理,可能是 ZonedDateTime 或 String |
||||
|
if (commit.getCommitTime() != null) { |
||||
|
if (commit.getCommitTime() instanceof ZonedDateTime) { |
||||
|
ZonedDateTime commitTime = (ZonedDateTime) commit.getCommitTime(); |
||||
|
commitData.put("timestamp", commitTime.toEpochSecond()); |
||||
|
commitData.put("time_str", commitTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); |
||||
|
} else { |
||||
|
// 如果是 String 类型,直接存储 |
||||
|
commitData.put("time_str", commit.getCommitTime().toString()); |
||||
|
try { |
||||
|
ZonedDateTime parsedTime = ZonedDateTime.parse(commit.getCommitTime().toString()); |
||||
|
commitData.put("timestamp", parsedTime.toEpochSecond()); |
||||
|
} catch (Exception e) { |
||||
|
commitData.put("timestamp", 0L); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
commitData.put("timestamp", 0L); |
||||
|
commitData.put("time_str", ""); |
||||
|
} |
||||
|
|
||||
|
// 存储文件变更信息 |
||||
|
if (commit.getFiles() != null && !commit.getFiles().isEmpty()) { |
||||
|
commitData.put("files_json", objectMapper.writeValueAsString(commit.getFiles())); |
||||
|
} |
||||
|
|
||||
|
// 存储统计信息 |
||||
|
if (commit.getStats() != null) { |
||||
|
Map<String, Object> stats = new HashMap<>(); |
||||
|
stats.put("total", commit.getStats().getTotal()); |
||||
|
stats.put("additions", commit.getStats().getAdditions()); |
||||
|
stats.put("deletions", commit.getStats().getDeletions()); |
||||
|
commitData.put("stats_json", objectMapper.writeValueAsString(stats)); |
||||
|
} |
||||
|
|
||||
|
redisCache.setCacheMap(commitKey, commitData); |
||||
|
|
||||
|
// 按日期建立索引 |
||||
|
try { |
||||
|
Long timestamp = (Long) commitData.get("timestamp"); |
||||
|
if (timestamp > 0) { |
||||
|
LocalDate commitDate = ZonedDateTime.ofInstant( |
||||
|
java.time.Instant.ofEpochSecond(timestamp), |
||||
|
java.time.ZoneId.systemDefault() |
||||
|
).toLocalDate(); |
||||
|
|
||||
|
String dateKey = COMMITS_BY_DATE_PREFIX + repoFullPath + ":" + commitDate.toString(); |
||||
|
|
||||
|
// 使用Set存储该日期下的所有提交SHA |
||||
|
Set<String> dateCommits = redisCache.getCacheSet(dateKey); |
||||
|
if (dateCommits == null) { |
||||
|
dateCommits = new HashSet<>(); |
||||
|
} |
||||
|
dateCommits.add(commit.getSha()); |
||||
|
redisCache.setCacheSet(dateKey, dateCommits); |
||||
|
|
||||
|
// 建立仓库-提交索引 |
||||
|
String repoCommitsKey = REPO_COMMITS_INDEX_PREFIX + repoFullPath; |
||||
|
Set<String> repoCommits = redisCache.getCacheSet(repoCommitsKey); |
||||
|
if (repoCommits == null) { |
||||
|
repoCommits = new HashSet<>(); |
||||
|
} |
||||
|
repoCommits.add(commit.getSha()); |
||||
|
redisCache.setCacheSet(repoCommitsKey, repoCommits); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.warn("建立提交索引失败: {}", e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 4. 拉取单个仓库的增量提交(昨天到现在的提交) |
||||
|
*/ |
||||
|
public void syncIncrementalCommitsForRepo(String repoFullPath) throws Exception { |
||||
|
log.info("开始增量同步仓库提交: {}", repoFullPath); |
||||
|
|
||||
|
// 获取昨天的时间 |
||||
|
LocalDate yesterday = LocalDate.now().minusDays(1); |
||||
|
ZonedDateTime since = yesterday.atStartOfDay(ZonedDateTime.now().getZone()); |
||||
|
ZonedDateTime until = ZonedDateTime.now(); |
||||
|
|
||||
|
String sinceStr = since.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
String untilStr = until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
||||
|
|
||||
|
String baseUrl = String.format("%s/api/v1/repos/%s/commits?since=%s&until=%s&limit=100", |
||||
|
giteaBaseUrl, repoFullPath, sinceStr, untilStr); |
||||
|
|
||||
|
List<GiteaCommit> newCommits = fetchWithPagination(baseUrl, GiteaCommit.class, 30); |
||||
|
|
||||
|
if (!newCommits.isEmpty()) { |
||||
|
log.info("仓库 {} 发现 {} 个新提交", repoFullPath, newCommits.size()); |
||||
|
|
||||
|
for (GiteaCommit commit : newCommits) { |
||||
|
storeCommit(repoFullPath, commit); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 5. 批量同步所有仓库的所有历史提交 |
||||
|
*/ |
||||
|
public void syncAllReposAllCommits() { |
||||
|
log.info("开始批量同步所有仓库的所有历史提交"); |
||||
|
|
||||
|
try { |
||||
|
// 1. 获取所有仓库 |
||||
|
List<GiteaRepository> repos = getAllRepositories(); |
||||
|
|
||||
|
if (repos.isEmpty()) { |
||||
|
log.warn("未找到任何仓库"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
log.info("共发现 {} 个仓库,开始同步历史提交", repos.size()); |
||||
|
|
||||
|
// 2. 记录同步进度 |
||||
|
Map<String, Object> progress = new HashMap<>(); |
||||
|
progress.put("totalRepos", repos.size()); |
||||
|
progress.put("completed", 0); |
||||
|
progress.put("startTime", System.currentTimeMillis()); |
||||
|
progress.put("status", "running"); |
||||
|
redisCache.setCacheObject(SYNC_PROGRESS_KEY, progress); |
||||
|
|
||||
|
// 3. 逐个仓库同步 |
||||
|
for (int i = 0; i < repos.size(); i++) { |
||||
|
GiteaRepository repo = repos.get(i); |
||||
|
|
||||
|
try { |
||||
|
log.info("正在同步仓库 {}/{}: {}", i + 1, repos.size(), repo.getFullPath()); |
||||
|
syncAllCommitsForRepo(repo.getFullPath()); |
||||
|
|
||||
|
// 更新进度 |
||||
|
progress.put("completed", i + 1); |
||||
|
progress.put("currentRepo", repo.getFullPath()); |
||||
|
redisCache.setCacheObject(SYNC_PROGRESS_KEY, progress); |
||||
|
|
||||
|
// 避免请求过快,间隔1秒 |
||||
|
Thread.sleep(1000); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("同步仓库 {} 失败: {}", repo.getFullPath(), e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 4. 完成同步 |
||||
|
progress.put("endTime", System.currentTimeMillis()); |
||||
|
progress.put("status", "completed"); |
||||
|
redisCache.setCacheObject(SYNC_PROGRESS_KEY, progress); |
||||
|
|
||||
|
log.info("所有仓库历史提交同步完成"); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("批量同步失败: {}", e.getMessage(), e); |
||||
|
|
||||
|
Map<String, Object> progress = new HashMap<>(); |
||||
|
progress.put("status", "failed"); |
||||
|
progress.put("error", e.getMessage()); |
||||
|
redisCache.setCacheObject(SYNC_PROGRESS_KEY, progress); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 6. 获取同步进度 |
||||
|
*/ |
||||
|
public Map<String, Object> getSyncProgress() { |
||||
|
Map<String, Object> progress = redisCache.getCacheObject(SYNC_PROGRESS_KEY); |
||||
|
if (progress == null) { |
||||
|
progress = new HashMap<>(); |
||||
|
progress.put("status", "not_started"); |
||||
|
} |
||||
|
return progress; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 7. 测试用的main方法 |
||||
|
*/ |
||||
|
public static void main(String[] args) { |
||||
|
// 这里模拟测试,实际使用时需要Spring上下文 |
||||
|
System.out.println("Gitea数据同步服务测试"); |
||||
|
System.out.println("主要功能:"); |
||||
|
System.out.println("1. getAllRepositories() - 获取所有仓库"); |
||||
|
System.out.println("2. syncAllCommitsForRepo() - 同步单个仓库所有历史提交"); |
||||
|
System.out.println("3. syncIncrementalCommitsForRepo() - 增量同步单个仓库"); |
||||
|
System.out.println("4. syncAllReposAllCommits() - 批量同步所有仓库所有历史提交"); |
||||
|
System.out.println("5. getSyncProgress() - 获取同步进度"); |
||||
|
|
||||
|
System.out.println("\n测试步骤:"); |
||||
|
System.out.println("1. 首先调用 getAllRepositories() 获取仓库列表"); |
||||
|
System.out.println("2. 调用 syncAllReposAllCommits() 同步所有历史数据"); |
||||
|
System.out.println("3. 调用 getSyncProgress() 查看同步进度"); |
||||
|
} |
||||
|
|
||||
|
// ==================== 工具方法 ==================== |
||||
|
|
||||
|
private String getAuthorName(GiteaCommit commit) { |
||||
|
if (commit.getCommit() != null && |
||||
|
commit.getCommit().getAuthor() != null && |
||||
|
commit.getCommit().getAuthor().getName() != null) { |
||||
|
return commit.getCommit().getAuthor().getName(); |
||||
|
} |
||||
|
return "未知作者"; |
||||
|
} |
||||
|
|
||||
|
private <T> List<T> fetchWithPagination(String baseUrl, Class<T> clazz, int timeoutSeconds) throws Exception { |
||||
|
List<T> results = new ArrayList<>(); |
||||
|
int page = 1; |
||||
|
|
||||
|
// 保护机制配置 |
||||
|
int maxTotalRecords = 100000; |
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
long maxDuration = 600000; |
||||
|
int consecutiveEmptyPages = 0; |
||||
|
int maxConsecutiveEmptyPages = 3; |
||||
|
|
||||
|
// 确保URL有limit参数,使用合理的limit |
||||
|
if (!baseUrl.contains("limit=")) { |
||||
|
baseUrl += (baseUrl.contains("?") ? "&" : "?") + "limit=50"; // 使用50,与Gitea默认一致 |
||||
|
} |
||||
|
|
||||
|
// 记录初始URL |
||||
|
String originalUrl = baseUrl; |
||||
|
log.info("开始分页获取数据,URL: {}", originalUrl); |
||||
|
|
||||
|
// 从URL获取请求的limit |
||||
|
int requestedLimit = getLimitFromUrl(baseUrl); |
||||
|
log.info("请求limit参数: {}", requestedLimit); |
||||
|
|
||||
|
// 重要:记录第一页的返回数量,用于后续判断 |
||||
|
Integer firstPageSize = null; |
||||
|
|
||||
|
while (true) { |
||||
|
// 保护机制检查... |
||||
|
if (results.size() >= maxTotalRecords) { |
||||
|
log.warn("达到最大记录数限制({}),停止分页", maxTotalRecords); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
long currentDuration = System.currentTimeMillis() - startTime; |
||||
|
if (currentDuration > maxDuration) { |
||||
|
log.warn("分页超时({}ms),停止分页", currentDuration); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (consecutiveEmptyPages >= maxConsecutiveEmptyPages) { |
||||
|
log.warn("连续{}页返回空数据,停止分页", consecutiveEmptyPages); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// 构造请求URL |
||||
|
String pageUrl = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "page=" + page; |
||||
|
|
||||
|
log.info("请求第{}页,累计{}条,耗时{}ms", page, results.size(), currentDuration); |
||||
|
|
||||
|
// 发送HTTP请求... |
||||
|
HttpRequest request = HttpRequest.newBuilder() |
||||
|
.uri(java.net.URI.create(pageUrl)) |
||||
|
.header("Authorization", "token " + accessToken) |
||||
|
.timeout(Duration.ofSeconds(30)) |
||||
|
.GET() |
||||
|
.build(); |
||||
|
|
||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); |
||||
|
|
||||
|
if (response.statusCode() != 200) { |
||||
|
log.warn("第{}页API请求失败: {} - {}", page, response.statusCode(), response.body()); |
||||
|
|
||||
|
// 如果是404,可能是仓库不存在或没权限 |
||||
|
if (response.statusCode() == 404) { |
||||
|
log.error("仓库可能不存在或无权访问,停止分页"); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// 等待后重试 |
||||
|
Thread.sleep(2000); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 解析响应数据 |
||||
|
List<T> pageResults = objectMapper.readValue( |
||||
|
response.body(), |
||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); |
||||
|
|
||||
|
if (pageResults == null) { |
||||
|
log.warn("第{}页解析结果为null", page); |
||||
|
consecutiveEmptyPages++; |
||||
|
page++; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (pageResults.isEmpty()) { |
||||
|
log.info("第{}页返回空数组,判断为最后一页", page); |
||||
|
break; // 空数组 = 最后一页 |
||||
|
} |
||||
|
|
||||
|
// 重置连续空页计数 |
||||
|
consecutiveEmptyPages = 0; |
||||
|
|
||||
|
// 记录第一页的大小 |
||||
|
if (firstPageSize == null) { |
||||
|
firstPageSize = pageResults.size(); |
||||
|
log.info("第一页返回{}条,请求的limit为{}条", firstPageSize, requestedLimit); |
||||
|
|
||||
|
// 重要判断:如果第一页就返回了少于请求的limit,并且数量很少,可能真的只有这么多 |
||||
|
// 但为了保险,我们还是继续请求下一页确认 |
||||
|
if (firstPageSize < requestedLimit && firstPageSize < 30) { |
||||
|
log.info("第一页返回较少数据({}条),继续请求下一页确认", firstPageSize); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 添加到结果集 |
||||
|
results.addAll(pageResults); |
||||
|
log.info("第{}页获取 {} 条,累计 {} 条", page, pageResults.size(), results.size()); |
||||
|
|
||||
|
// ==================== 关键修改:判断是否最后一页 ==================== |
||||
|
// 方案:总是继续请求下一页,直到返回空数组 |
||||
|
|
||||
|
// 但可以添加一些智能判断: |
||||
|
// 1. 如果连续几页都返回相同数量的数据,可能还有更多 |
||||
|
// 2. 如果返回数量突然减少,可能是最后一页的迹象(但不绝对) |
||||
|
|
||||
|
// 准备下一页 |
||||
|
page++; |
||||
|
|
||||
|
// 动态休眠 |
||||
|
int sleepTime = calculateSleepTime(page, results.size()); |
||||
|
Thread.sleep(sleepTime); |
||||
|
|
||||
|
// 保护:最多1000页 |
||||
|
if (page > 1000) { |
||||
|
log.warn("达到最大页数限制(1000页),强制停止分页"); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
long totalDuration = System.currentTimeMillis() - startTime; |
||||
|
log.info("分页结束,总共获取 {} 条数据,耗时 {}ms (约{:.1f}秒)", |
||||
|
results.size(), totalDuration, totalDuration / 1000.0); |
||||
|
|
||||
|
return results; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算休眠时间 |
||||
|
*/ |
||||
|
private int calculateSleepTime(int currentPage, int totalRecords) { |
||||
|
int baseSleep = 300; |
||||
|
|
||||
|
// 根据页数增加 |
||||
|
int pageFactor = (currentPage / 10) * 100; |
||||
|
|
||||
|
// 根据总记录数增加 |
||||
|
int recordFactor = (totalRecords / 1000) * 50; |
||||
|
|
||||
|
int sleepTime = baseSleep + pageFactor + recordFactor; |
||||
|
|
||||
|
// 限制范围 |
||||
|
sleepTime = Math.max(200, Math.min(sleepTime, 3000)); |
||||
|
|
||||
|
return sleepTime; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 从URL中提取limit参数值 |
||||
|
*/ |
||||
|
private int getLimitFromUrl(String url) { |
||||
|
try { |
||||
|
if (url.contains("limit=")) { |
||||
|
String[] parts = url.split("limit="); |
||||
|
if (parts.length > 1) { |
||||
|
String limitStr = parts[1]; |
||||
|
// 移除后面的参数 |
||||
|
if (limitStr.contains("&")) { |
||||
|
limitStr = limitStr.substring(0, limitStr.indexOf('&')); |
||||
|
} |
||||
|
if (limitStr.contains("?")) { |
||||
|
limitStr = limitStr.substring(0, limitStr.indexOf('?')); |
||||
|
} |
||||
|
return Integer.parseInt(limitStr.trim()); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.warn("解析limit参数失败,使用默认值100。URL: {}", url); |
||||
|
} |
||||
|
return 100; // 默认值 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 8. 真正的测试方法 - 执行实际同步并检查Redis |
||||
|
*/ |
||||
|
public void realSyncTest() { |
||||
|
System.out.println("=== 开始真实同步测试 ==="); |
||||
|
|
||||
|
try { |
||||
|
// 1. 获取仓库 |
||||
|
System.out.println("1. 获取仓库列表..."); |
||||
|
List<GiteaRepository> repos = getAllRepositories(); |
||||
|
System.out.println("获取到 " + repos.size() + " 个仓库"); |
||||
|
|
||||
|
if (repos.isEmpty()) { |
||||
|
System.out.println("没有仓库,测试结束"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 2. 选择第一个仓库进行测试 |
||||
|
String testRepo = repos.get(0).getFullPath(); |
||||
|
System.out.println("2. 测试同步仓库: " + testRepo); |
||||
|
|
||||
|
// 3. 同步该仓库的历史提交 |
||||
|
System.out.println("3. 开始同步历史提交..."); |
||||
|
syncAllCommitsForRepo(testRepo); |
||||
|
|
||||
|
// 4. 检查Redis中的数据 |
||||
|
System.out.println("4. 检查Redis中的数据..."); |
||||
|
|
||||
|
// 检查仓库信息 |
||||
|
String repoKey = REPO_INFO_PREFIX + testRepo; |
||||
|
Map<String, Object> repoInfo = redisCache.getCacheMap(repoKey); |
||||
|
System.out.println(" 仓库信息: " + (repoInfo != null ? "存在" : "不存在")); |
||||
|
if (repoInfo != null) { |
||||
|
System.out.println(" 仓库名称: " + repoInfo.get("name")); |
||||
|
System.out.println(" 仓库路径: " + repoInfo.get("fullPath")); |
||||
|
} |
||||
|
|
||||
|
// 检查仓库提交索引 |
||||
|
String repoCommitsKey = REPO_COMMITS_INDEX_PREFIX + testRepo; |
||||
|
Set<String> commitShas = redisCache.getCacheSet(repoCommitsKey); |
||||
|
System.out.println(" 提交数量: " + (commitShas != null ? commitShas.size() : 0)); |
||||
|
|
||||
|
if (commitShas != null && !commitShas.isEmpty()) { |
||||
|
// 随机检查一个提交的详情 |
||||
|
String sampleSha = commitShas.iterator().next(); |
||||
|
String commitKey = COMMIT_PREFIX + testRepo + ":" + sampleSha; |
||||
|
Map<String, Object> commitData = redisCache.getCacheMap(commitKey); |
||||
|
|
||||
|
System.out.println(" 示例提交SHA: " + sampleSha.substring(0, 8) + "..."); |
||||
|
if (commitData != null) { |
||||
|
System.out.println(" 作者: " + commitData.get("author")); |
||||
|
System.out.println(" 时间: " + commitData.get("time_str")); |
||||
|
System.out.println(" 消息: " + |
||||
|
(commitData.get("message") != null ? |
||||
|
commitData.get("message").toString().substring(0, Math.min(50, commitData.get("message").toString().length())) + "..." : "无")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 5. 测试增量同步 |
||||
|
System.out.println("5. 测试增量同步..."); |
||||
|
syncIncrementalCommitsForRepo(testRepo); |
||||
|
System.out.println(" 增量同步完成"); |
||||
|
|
||||
|
// 6. 重新检查提交数量 |
||||
|
commitShas = redisCache.getCacheSet(repoCommitsKey); |
||||
|
System.out.println(" 最终提交数量: " + (commitShas != null ? commitShas.size() : 0)); |
||||
|
|
||||
|
System.out.println("\n=== 测试完成 ==="); |
||||
|
System.out.println("\n你可以使用以下命令检查Redis数据:"); |
||||
|
System.out.println("redis-cli keys 'gitea:*" + testRepo + "*'"); |
||||
|
System.out.println("redis-cli hgetall 'gitea:repo:" + testRepo + "'"); |
||||
|
System.out.println("redis-cli smembers 'gitea:repo_commits:" + testRepo + "'"); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.println("测试失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 真正的全量测试 - 同步所有仓库并检查数据 |
||||
|
*/ |
||||
|
public void fullSyncTest() { |
||||
|
System.out.println("=== 开始全量同步测试 ==="); |
||||
|
System.out.println("这将同步所有183个仓库的历史提交到Redis"); |
||||
|
|
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
|
||||
|
try { |
||||
|
// 1. 获取所有仓库 |
||||
|
System.out.println("1. 获取仓库列表..."); |
||||
|
List<GiteaRepository> repos = getAllRepositories(); |
||||
|
System.out.println("获取到 " + repos.size() + " 个仓库"); |
||||
|
|
||||
|
if (repos.isEmpty()) { |
||||
|
System.out.println("没有仓库,测试结束"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 2. 同步前5个仓库(先测试一部分) |
||||
|
System.out.println("2. 测试同步前5个仓库..."); |
||||
|
int testCount = Math.min(5, repos.size()); |
||||
|
int totalCommits = 0; |
||||
|
|
||||
|
for (int i = 0; i < testCount; i++) { |
||||
|
GiteaRepository repo = repos.get(i); |
||||
|
System.out.println(" 同步仓库 " + (i+1) + "/" + testCount + ": " + repo.getFullPath()); |
||||
|
|
||||
|
try { |
||||
|
// 先检查Redis中已有的提交数 |
||||
|
String repoCommitsKey = REPO_COMMITS_INDEX_PREFIX + repo.getFullPath(); |
||||
|
Set<String> beforeCommits = redisCache.getCacheSet(repoCommitsKey); |
||||
|
int beforeCount = beforeCommits != null ? beforeCommits.size() : 0; |
||||
|
|
||||
|
// 同步该仓库 |
||||
|
syncAllCommitsForRepo(repo.getFullPath()); |
||||
|
|
||||
|
// 检查同步后的提交数 |
||||
|
Set<String> afterCommits = redisCache.getCacheSet(repoCommitsKey); |
||||
|
int afterCount = afterCommits != null ? afterCommits.size() : 0; |
||||
|
int newCommits = afterCount - beforeCount; |
||||
|
|
||||
|
totalCommits += afterCount; |
||||
|
System.out.println(" 同步前: " + beforeCount + " 条,同步后: " + afterCount + " 条,新增: " + newCommits + " 条"); |
||||
|
|
||||
|
// 间隔1秒,避免请求过快 |
||||
|
Thread.sleep(1000); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.println(" 仓库同步失败: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 3. 检查整体数据 |
||||
|
System.out.println("3. 检查整体Redis数据..."); |
||||
|
|
||||
|
// 统计所有仓库的提交总数 |
||||
|
int allRepoCommits = 0; |
||||
|
Map<String, String> allRepos = redisCache.getCacheMap(REPO_LIST_KEY); |
||||
|
if (allRepos != null) { |
||||
|
System.out.println(" Redis中仓库总数: " + allRepos.size()); |
||||
|
|
||||
|
// 抽样检查几个仓库 |
||||
|
int sampleCount = Math.min(10, allRepos.size()); |
||||
|
int checked = 0; |
||||
|
|
||||
|
for (Map.Entry<String, String> entry : allRepos.entrySet()) { |
||||
|
if (checked >= sampleCount) break; |
||||
|
|
||||
|
String repoFullPath = entry.getValue(); |
||||
|
String repoCommitsKey = REPO_COMMITS_INDEX_PREFIX + repoFullPath; |
||||
|
Set<String> commits = redisCache.getCacheSet(repoCommitsKey); |
||||
|
|
||||
|
if (commits != null) { |
||||
|
allRepoCommits += commits.size(); |
||||
|
checked++; |
||||
|
|
||||
|
if (checked <= 3) { // 只打印前3个的详情 |
||||
|
System.out.println(" 仓库 " + checked + ": " + repoFullPath + " - " + commits.size() + " 个提交"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (allRepos.size() > 3) { |
||||
|
System.out.println(" 还有 " + (allRepos.size() - 3) + " 个仓库..."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 4. 统计Redis中所有提交key |
||||
|
System.out.println("4. 统计Redis中所有Gitea相关数据..."); |
||||
|
// 注意:keys命令在生产环境要谨慎使用,这里只是测试 |
||||
|
System.out.println(" 请手动运行: redis-cli keys 'gitea:commit:*' | wc -l"); |
||||
|
System.out.println(" 请手动运行: redis-cli keys 'gitea:repo:*' | wc -l"); |
||||
|
|
||||
|
long endTime = System.currentTimeMillis(); |
||||
|
long duration = (endTime - startTime) / 1000; |
||||
|
|
||||
|
System.out.println("\n=== 测试完成 ==="); |
||||
|
System.out.println("总耗时: " + duration + " 秒"); |
||||
|
System.out.println("测试仓库数: " + testCount); |
||||
|
System.out.println("总提交数: " + totalCommits); |
||||
|
System.out.println("Redis中预估总提交数: " + allRepoCommits); |
||||
|
|
||||
|
System.out.println("\n手动检查命令:"); |
||||
|
System.out.println(" # 查看所有仓库"); |
||||
|
System.out.println(" redis-cli hgetall 'gitea:repos:list' | head -20"); |
||||
|
System.out.println(" "); |
||||
|
System.out.println(" # 查看提交总数"); |
||||
|
System.out.println(" redis-cli keys 'gitea:commit:*' | wc -l"); |
||||
|
System.out.println(" "); |
||||
|
System.out.println(" # 查看某个仓库的提交"); |
||||
|
System.out.println(" redis-cli smembers 'gitea:repo_commits:ChenHaiTech/仓库名' | wc -l"); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.println("全量测试失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 执行真正的全量同步(所有仓库) |
||||
|
*/ |
||||
|
public void executeFullSync() { |
||||
|
System.out.println("=== 开始执行全量同步 ==="); |
||||
|
System.out.println("警告:这将同步所有183个仓库,耗时可能较长!"); |
||||
|
System.out.println("建议在夜间或低峰期执行"); |
||||
|
|
||||
|
new Thread(() -> { |
||||
|
try { |
||||
|
syncAllReposAllCommits(); |
||||
|
System.out.println("全量同步完成!"); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("全量同步失败: " + e.getMessage()); |
||||
|
} |
||||
|
}).start(); |
||||
|
|
||||
|
System.out.println("全量同步已异步启动,请查看日志..."); |
||||
|
System.out.println("查看进度: GET /test/gitea/progress"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,946 @@ |
|||||
|
package com.chenhai.chenhaiai.service.gitNew; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.*; |
||||
|
import com.chenhai.common.core.redis.RedisCache; |
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.time.*; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.time.temporal.ChronoUnit; |
||||
|
import java.time.temporal.WeekFields; |
||||
|
import java.util.*; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class GiteaGranularityService { |
||||
|
|
||||
|
@Autowired |
||||
|
private GiteaQueryService giteaQueryService; |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisCache redisCache; |
||||
|
|
||||
|
private final ObjectMapper objectMapper; |
||||
|
|
||||
|
// Redis Key常量(复用GiteaQueryService的常量) |
||||
|
private static final String REPO_LIST_KEY = "gitea:repos:list"; |
||||
|
private static final String REPO_INFO_KEY_PREFIX = "gitea:repo:"; |
||||
|
private static final String COMMIT_KEY_PREFIX = "gitea:commit:"; |
||||
|
private static final String COMMITS_BY_DATE_KEY_PREFIX = "gitea:commits:by_date:"; |
||||
|
|
||||
|
// 时间格式化器 |
||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
||||
|
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); |
||||
|
private static final DateTimeFormatter YEAR_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); |
||||
|
|
||||
|
public GiteaGranularityService() { |
||||
|
this.objectMapper = new ObjectMapper(); |
||||
|
this.objectMapper.registerModule(new JavaTimeModule()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取带颗粒度的文本分析报告 |
||||
|
* @param since 开始时间(yyyy-MM-dd) |
||||
|
* @param until 结束时间(yyyy-MM-dd) |
||||
|
* @param granularity 颗粒度:day/week/month/year/auto |
||||
|
* @return 分析报告 |
||||
|
*/ |
||||
|
public Map<String, Object> getTextAnalysisReportWithGranularity(String since, String until, String granularity) { |
||||
|
Map<String, Object> result = new HashMap<>(); |
||||
|
|
||||
|
try { |
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
|
||||
|
// 1. 获取原始提交数据 |
||||
|
List<CommitDetail> allCommits = getAllCommitsInRange(since, until); |
||||
|
|
||||
|
// 2. 根据原始数据生成分析数据 |
||||
|
GitAnalysisData data = analyzeCommitsData(allCommits, since, until); |
||||
|
|
||||
|
// 3. 确定实际使用的颗粒度 |
||||
|
long daysBetween = calculateDaysBetween(since, until); |
||||
|
String actualGranularity = determineActualGranularity(granularity, daysBetween); |
||||
|
|
||||
|
// 4. 生成带颗粒度的报告 |
||||
|
String textReport = generateTextReportWithGranularity(data, allCommits, since, until, actualGranularity); |
||||
|
|
||||
|
long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
|
||||
|
result.put("success", true); |
||||
|
result.put("report", textReport); |
||||
|
result.put("rawData", data); |
||||
|
result.put("granularity", actualGranularity); |
||||
|
result.put("availableGranularities", getAvailableGranularities(daysBetween)); |
||||
|
result.put("generatedTime", LocalDate.now().format(DATE_FORMATTER)); |
||||
|
result.put("analysisTime", analysisTime); |
||||
|
result.put("totalCommits", allCommits.size()); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("生成带颗粒度报告失败: {}", e.getMessage(), e); |
||||
|
result.put("success", false); |
||||
|
result.put("error", e.getMessage()); |
||||
|
result.put("report", "生成报告失败: " + e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取时间范围内的所有提交 |
||||
|
*/ |
||||
|
private List<CommitDetail> getAllCommitsInRange(String since, String until) { |
||||
|
List<CommitDetail> allCommits = new ArrayList<>(); |
||||
|
LocalDate startDate = LocalDate.parse(since, DATE_FORMATTER); |
||||
|
LocalDate endDate = LocalDate.parse(until, DATE_FORMATTER); |
||||
|
|
||||
|
// 获取所有仓库 |
||||
|
Map<String, String> allRepos = redisCache.getCacheMap(REPO_LIST_KEY); |
||||
|
if (allRepos == null || allRepos.isEmpty()) { |
||||
|
return allCommits; |
||||
|
} |
||||
|
|
||||
|
// 遍历所有仓库 |
||||
|
for (Map.Entry<String, String> entry : allRepos.entrySet()) { |
||||
|
String repoFullPath = entry.getValue(); |
||||
|
|
||||
|
// 遍历日期范围内的每一天 |
||||
|
LocalDate currentDate = startDate; |
||||
|
while (!currentDate.isAfter(endDate)) { |
||||
|
String dateKey = COMMITS_BY_DATE_KEY_PREFIX + repoFullPath + ":" + currentDate; |
||||
|
Set<String> dateCommits = redisCache.getCacheSet(dateKey); |
||||
|
|
||||
|
if (dateCommits != null && !dateCommits.isEmpty()) { |
||||
|
for (String sha : dateCommits) { |
||||
|
try { |
||||
|
String commitKey = COMMIT_KEY_PREFIX + repoFullPath + ":" + sha; |
||||
|
Map<String, Object> commitData = redisCache.getCacheMap(commitKey); |
||||
|
|
||||
|
if (commitData != null) { |
||||
|
CommitDetail detail = convertToCommitDetail(commitData, repoFullPath); |
||||
|
if (detail != null) { |
||||
|
allCommits.add(detail); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.debug("获取提交详情失败: {}", e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
currentDate = currentDate.plusDays(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
log.info("获取到 {} 条提交数据 ({} 至 {})", allCommits.size(), since, until); |
||||
|
return allCommits; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 转换为提交详情 |
||||
|
*/ |
||||
|
private CommitDetail convertToCommitDetail(Map<String, Object> commitData, String repoFullPath) { |
||||
|
try { |
||||
|
CommitDetail detail = new CommitDetail(); |
||||
|
detail.setSha(commitData.get("sha") != null ? commitData.get("sha").toString() : null); |
||||
|
detail.setAuthor(commitData.get("author") != null ? commitData.get("author").toString() : null); |
||||
|
detail.setMessage(commitData.get("message") != null ? commitData.get("message").toString() : null); |
||||
|
detail.setRepoName(repoFullPath); |
||||
|
|
||||
|
// 获取文件信息 |
||||
|
if (commitData.get("files_json") != null) { |
||||
|
detail.setFilesJson(commitData.get("files_json").toString()); |
||||
|
} |
||||
|
|
||||
|
if (commitData.get("timestamp") != null) { |
||||
|
try { |
||||
|
long timestamp = ((Number) commitData.get("timestamp")).longValue(); |
||||
|
detail.setCommitTime(ZonedDateTime.ofInstant( |
||||
|
Instant.ofEpochSecond(timestamp), |
||||
|
ZoneId.systemDefault() |
||||
|
)); |
||||
|
} catch (ClassCastException e) { |
||||
|
log.warn("时间戳格式错误: {}", commitData.get("timestamp")); |
||||
|
return null; |
||||
|
} |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return detail; |
||||
|
} catch (Exception e) { |
||||
|
log.warn("转换提交详情失败: {}", e.getMessage()); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 分析提交数据 |
||||
|
*/ |
||||
|
private GitAnalysisData analyzeCommitsData(List<CommitDetail> commits, String since, String until) { |
||||
|
GitAnalysisData data = new GitAnalysisData(); |
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
|
||||
|
// 基础统计 |
||||
|
int totalCommits = commits.size(); |
||||
|
Set<String> developers = new HashSet<>(); |
||||
|
Set<String> repos = new HashSet<>(); |
||||
|
Map<String, Integer> devCommitCount = new HashMap<>(); |
||||
|
Map<String, Integer> repoCommitCount = new HashMap<>(); |
||||
|
Map<String, Set<String>> devRepos = new HashMap<>(); |
||||
|
Map<String, Set<String>> repoDevs = new HashMap<>(); |
||||
|
|
||||
|
// 按星期统计 |
||||
|
Map<DayOfWeek, Integer> dayStats = new HashMap<>(); |
||||
|
|
||||
|
// 文件类型统计 |
||||
|
Map<String, Integer> fileTypeStats = new HashMap<>(); |
||||
|
|
||||
|
for (CommitDetail commit : commits) { |
||||
|
// 开发者统计 |
||||
|
if (commit.getAuthor() != null) { |
||||
|
developers.add(commit.getAuthor()); |
||||
|
devCommitCount.merge(commit.getAuthor(), 1, Integer::sum); |
||||
|
|
||||
|
// 开发者参与仓库 |
||||
|
devRepos.computeIfAbsent(commit.getAuthor(), k -> new HashSet<>()) |
||||
|
.add(commit.getRepoName()); |
||||
|
} |
||||
|
|
||||
|
// 仓库统计 |
||||
|
if (commit.getRepoName() != null) { |
||||
|
repos.add(commit.getRepoName()); |
||||
|
repoCommitCount.merge(commit.getRepoName(), 1, Integer::sum); |
||||
|
|
||||
|
// 仓库的开发者 |
||||
|
repoDevs.computeIfAbsent(commit.getRepoName(), k -> new HashSet<>()) |
||||
|
.add(commit.getAuthor()); |
||||
|
} |
||||
|
|
||||
|
// 星期统计 |
||||
|
if (commit.getCommitTime() != null) { |
||||
|
DayOfWeek day = commit.getCommitTime().getDayOfWeek(); |
||||
|
dayStats.merge(day, 1, Integer::sum); |
||||
|
} |
||||
|
|
||||
|
// 文件类型统计 |
||||
|
if (commit.getFilesJson() != null && !commit.getFilesJson().isEmpty()) { |
||||
|
try { |
||||
|
List<Map<String, String>> files = objectMapper.readValue( |
||||
|
commit.getFilesJson(), new TypeReference<List<Map<String, String>>>() {}); |
||||
|
|
||||
|
for (Map<String, String> file : files) { |
||||
|
String filename = file.get("filename"); |
||||
|
if (filename != null) { |
||||
|
String fileType = getFileType(filename); |
||||
|
fileTypeStats.merge(fileType, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.debug("解析文件JSON失败: {}", e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取总仓库数 |
||||
|
Map<String, String> allReposMap = redisCache.getCacheMap(REPO_LIST_KEY); |
||||
|
int totalRepos = allReposMap != null ? allReposMap.size() : 0; |
||||
|
|
||||
|
// 构建基础信息 |
||||
|
BasicInfo basicInfo = new BasicInfo( |
||||
|
since + " 至 " + until, |
||||
|
totalRepos, // 总仓库数 |
||||
|
repos.size(), // 活跃仓库数 |
||||
|
developers.size(), // 活跃开发者数 |
||||
|
totalCommits, // 总提交数 |
||||
|
System.currentTimeMillis() - startTime, // 分析耗时 |
||||
|
"Redis缓存查询(带颗粒度)" |
||||
|
); |
||||
|
data.setBasicInfo(basicInfo); |
||||
|
|
||||
|
// 构建开发者排行榜 |
||||
|
List<DeveloperRank> developerRanks = buildDeveloperRanks(devCommitCount, devRepos); |
||||
|
data.setDeveloperRanks(developerRanks); |
||||
|
|
||||
|
// 构建仓库排行榜 |
||||
|
List<RepoRank> repoRanks = buildRepoRanks(repoCommitCount, repoDevs); |
||||
|
data.setRepoRanks(repoRanks); |
||||
|
|
||||
|
// 构建星期分布 |
||||
|
List<DayStats> dayStatsList = buildDayStats(dayStats); |
||||
|
data.setDayStats(dayStatsList); |
||||
|
|
||||
|
// 构建文件类型统计 |
||||
|
List<FileTypeStats> fileTypeStatsList = buildFileTypeStats(fileTypeStats); |
||||
|
data.setFileTypeStats(fileTypeStatsList); |
||||
|
|
||||
|
data.setGeneratedTime(LocalDate.now().format(DATE_FORMATTER)); |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取文件类型 |
||||
|
*/ |
||||
|
private String getFileType(String filename) { |
||||
|
if (filename == null || filename.isEmpty()) { |
||||
|
return "未知"; |
||||
|
} |
||||
|
|
||||
|
int dotIndex = filename.lastIndexOf('.'); |
||||
|
if (dotIndex > 0 && dotIndex < filename.length() - 1) { |
||||
|
String ext = filename.substring(dotIndex + 1).toLowerCase(); |
||||
|
|
||||
|
Map<String, String> typeMap = new HashMap<>(); |
||||
|
typeMap.put("java", "Java"); |
||||
|
typeMap.put("py", "Python"); |
||||
|
typeMap.put("js", "JavaScript"); |
||||
|
typeMap.put("ts", "TypeScript"); |
||||
|
typeMap.put("vue", "Vue"); |
||||
|
typeMap.put("html", "HTML"); |
||||
|
typeMap.put("css", "CSS"); |
||||
|
typeMap.put("md", "Markdown"); |
||||
|
typeMap.put("json", "JSON"); |
||||
|
typeMap.put("yml", "YAML"); |
||||
|
typeMap.put("yaml", "YAML"); |
||||
|
typeMap.put("xml", "XML"); |
||||
|
typeMap.put("sql", "SQL"); |
||||
|
typeMap.put("sh", "Shell"); |
||||
|
|
||||
|
return typeMap.getOrDefault(ext, ext.toUpperCase()); |
||||
|
} |
||||
|
|
||||
|
return "无扩展名"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建开发者排行榜 |
||||
|
*/ |
||||
|
private List<DeveloperRank> buildDeveloperRanks(Map<String, Integer> devCommitCount, |
||||
|
Map<String, Set<String>> devRepos) { |
||||
|
List<Map.Entry<String, Integer>> sortedDevs = new ArrayList<>(devCommitCount.entrySet()); |
||||
|
sortedDevs.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); |
||||
|
|
||||
|
List<DeveloperRank> ranks = new ArrayList<>(); |
||||
|
int rank = 1; |
||||
|
for (Map.Entry<String, Integer> entry : sortedDevs) { |
||||
|
if (rank > 15) break; |
||||
|
|
||||
|
String devName = entry.getKey(); |
||||
|
int commitCount = entry.getValue(); |
||||
|
int repoCount = devRepos.getOrDefault(devName, Collections.emptySet()).size(); |
||||
|
|
||||
|
ranks.add(new DeveloperRank(rank++, devName, commitCount, repoCount)); |
||||
|
} |
||||
|
return ranks; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建仓库排行榜 |
||||
|
*/ |
||||
|
private List<RepoRank> buildRepoRanks(Map<String, Integer> repoCommitCount, |
||||
|
Map<String, Set<String>> repoDevs) { |
||||
|
List<Map.Entry<String, Integer>> sortedRepos = new ArrayList<>(repoCommitCount.entrySet()); |
||||
|
sortedRepos.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); |
||||
|
|
||||
|
List<RepoRank> ranks = new ArrayList<>(); |
||||
|
int rank = 1; |
||||
|
for (Map.Entry<String, Integer> entry : sortedRepos) { |
||||
|
if (rank > 15) break; |
||||
|
|
||||
|
String repoName = entry.getKey(); |
||||
|
int commitCount = entry.getValue(); |
||||
|
int devCount = repoDevs.getOrDefault(repoName, Collections.emptySet()).size(); |
||||
|
|
||||
|
// 获取仓库显示名称 |
||||
|
String repoKey = REPO_INFO_KEY_PREFIX + repoName; |
||||
|
Map<String, Object> repoInfo = redisCache.getCacheMap(repoKey); |
||||
|
String displayName = repoInfo != null && repoInfo.get("name") != null |
||||
|
? repoInfo.get("name").toString() |
||||
|
: repoName; |
||||
|
|
||||
|
ranks.add(new RepoRank(rank++, repoName, displayName, commitCount, devCount)); |
||||
|
} |
||||
|
return ranks; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建星期分布 |
||||
|
*/ |
||||
|
private List<DayStats> buildDayStats(Map<DayOfWeek, Integer> dayStatsMap) { |
||||
|
List<DayStats> statsList = new ArrayList<>(); |
||||
|
String[] dayNames = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; |
||||
|
DayOfWeek[] days = DayOfWeek.values(); |
||||
|
|
||||
|
for (int i = 0; i < 7; i++) { |
||||
|
int count = dayStatsMap.getOrDefault(days[i], 0); |
||||
|
statsList.add(new DayStats(dayNames[i], count)); |
||||
|
} |
||||
|
return statsList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建文件类型统计 |
||||
|
*/ |
||||
|
private List<FileTypeStats> buildFileTypeStats(Map<String, Integer> fileTypeStatsMap) { |
||||
|
List<FileTypeStats> statsList = new ArrayList<>(); |
||||
|
|
||||
|
if (fileTypeStatsMap != null && !fileTypeStatsMap.isEmpty()) { |
||||
|
List<Map.Entry<String, Integer>> sortedEntries = new ArrayList<>(fileTypeStatsMap.entrySet()); |
||||
|
sortedEntries.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); |
||||
|
|
||||
|
int rank = 1; |
||||
|
for (Map.Entry<String, Integer> entry : sortedEntries) { |
||||
|
if (rank > 15) break; |
||||
|
statsList.add(new FileTypeStats(entry.getKey(), entry.getValue())); |
||||
|
rank++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return statsList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 确定实际颗粒度 |
||||
|
*/ |
||||
|
private String determineActualGranularity(String requestedGranularity, long daysBetween) { |
||||
|
// 如果请求auto,则自动选择 |
||||
|
if ("auto".equalsIgnoreCase(requestedGranularity)) { |
||||
|
return autoSelectGranularity(daysBetween); |
||||
|
} |
||||
|
|
||||
|
// 检查请求的颗粒度是否可行 |
||||
|
return validateGranularity(requestedGranularity, daysBetween); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 自动选择颗粒度 |
||||
|
*/ |
||||
|
private String autoSelectGranularity(long daysBetween) { |
||||
|
if (daysBetween <= 7) { |
||||
|
return "day"; // 一周内:按天 |
||||
|
} else if (daysBetween <= 30) { |
||||
|
return "week"; // 一个月内:按周 |
||||
|
} else if (daysBetween <= 365) { |
||||
|
return "month"; // 一年内:按月 |
||||
|
} else { |
||||
|
return "year"; // 超过一年:按年 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 验证颗粒度 |
||||
|
*/ |
||||
|
private String validateGranularity(String requested, long daysBetween) { |
||||
|
switch (requested.toLowerCase()) { |
||||
|
case "day": |
||||
|
if (daysBetween > 30) { |
||||
|
log.warn("时间范围{}天超过30天,day颗粒度不可用,自动降级为week", daysBetween); |
||||
|
return validateGranularity("week", daysBetween); |
||||
|
} |
||||
|
return "day"; |
||||
|
|
||||
|
case "week": |
||||
|
if (daysBetween > 90) { |
||||
|
log.warn("时间范围{}天超过90天,week颗粒度不可用,自动降级为month", daysBetween); |
||||
|
return validateGranularity("month", daysBetween); |
||||
|
} |
||||
|
return "week"; |
||||
|
|
||||
|
case "month": |
||||
|
if (daysBetween > 365 * 2) { |
||||
|
log.warn("时间范围{}天超过2年,month颗粒度不可用,自动降级为year", daysBetween); |
||||
|
return "year"; |
||||
|
} |
||||
|
return "month"; |
||||
|
|
||||
|
case "year": |
||||
|
return "year"; |
||||
|
|
||||
|
default: |
||||
|
log.warn("未知颗粒度: {},使用auto", requested); |
||||
|
return autoSelectGranularity(daysBetween); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取可用颗粒度列表 |
||||
|
*/ |
||||
|
private List<String> getAvailableGranularities(long daysBetween) { |
||||
|
List<String> available = new ArrayList<>(); |
||||
|
available.add("auto"); |
||||
|
|
||||
|
if (daysBetween <= 30) { |
||||
|
available.add("day"); |
||||
|
} |
||||
|
if (daysBetween <= 90) { |
||||
|
available.add("week"); |
||||
|
} |
||||
|
if (daysBetween <= 365 * 2) { |
||||
|
available.add("month"); |
||||
|
} |
||||
|
available.add("year"); |
||||
|
|
||||
|
return available; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成带颗粒度的报告 |
||||
|
*/ |
||||
|
private String generateTextReportWithGranularity(GitAnalysisData data, List<CommitDetail> commits, |
||||
|
String since, String until, String granularity) { |
||||
|
StringBuilder sb = new StringBuilder(); |
||||
|
|
||||
|
long daysBetween = calculateDaysBetween(since, until); |
||||
|
|
||||
|
sb.append("======================================================================\n"); |
||||
|
sb.append(" Gitea代码提交分析报告 (颗粒度: ").append(granularity).append(")\n"); |
||||
|
sb.append("======================================================================\n\n"); |
||||
|
|
||||
|
sb.append("📅 分析时间范围: ").append(since).append(" 至 ").append(until); |
||||
|
sb.append(" (").append(daysBetween).append("天)\n"); |
||||
|
sb.append("⏱️ 查询耗时: ").append(String.format("%.2f", data.getBasicInfo().getAnalysisTime() / 1000.0)) |
||||
|
.append("秒 | 📊 数据来源: Redis缓存\n\n"); |
||||
|
|
||||
|
// 基础信息 |
||||
|
sb.append("📊 总体概览:\n"); |
||||
|
sb.append("├── 📦 总仓库数: ").append(data.getBasicInfo().getTotalRepos()).append(" 个\n"); |
||||
|
sb.append("├── ⭐ 活跃仓库: ").append(data.getBasicInfo().getActiveRepos()).append(" 个\n"); |
||||
|
sb.append("├── 👥 活跃开发者: ").append(data.getBasicInfo().getActiveDevelopers()).append(" 人\n"); |
||||
|
sb.append("├── 📝 总提交次数: ").append(data.getBasicInfo().getTotalCommits()).append(" 次\n"); |
||||
|
sb.append("└── 📈 日均提交: ").append(String.format("%.1f", data.getBasicInfo().getTotalCommits() * 1.0 / Math.max(1, daysBetween))) |
||||
|
.append(" 次/天\n\n"); |
||||
|
|
||||
|
// 时间分布(按颗粒度) |
||||
|
sb.append("📈 提交时间分布(按").append(granularity).append("):\n"); |
||||
|
|
||||
|
switch (granularity) { |
||||
|
case "day": |
||||
|
generateDailyDistribution(sb, commits, since, until); |
||||
|
break; |
||||
|
case "week": |
||||
|
generateWeeklyDistribution(sb, commits, since, until); |
||||
|
break; |
||||
|
case "month": |
||||
|
generateMonthlyDistribution(sb, commits, since, until); |
||||
|
break; |
||||
|
case "year": |
||||
|
generateYearlyDistribution(sb, commits, since, until); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
sb.append("\n"); |
||||
|
|
||||
|
// 开发者排行榜 |
||||
|
if (data.getDeveloperRanks() != null && !data.getDeveloperRanks().isEmpty()) { |
||||
|
sb.append("🏆 开发者排行榜 (按提交次数):\n"); |
||||
|
sb.append("┌────┬────────────┬────────────┬──────────┬────────────┐\n"); |
||||
|
sb.append("│排名│ 开发者 │ 提交次数 │ 参与仓库 │ 活跃度 │\n"); |
||||
|
sb.append("├────┼────────────┼────────────┼──────────┼────────────┤\n"); |
||||
|
|
||||
|
int displayCount = Math.min(10, data.getDeveloperRanks().size()); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
DeveloperRank rank = data.getDeveloperRanks().get(i); |
||||
|
String stars = getStars(rank.getCommitCount(), data.getBasicInfo().getTotalCommits()); |
||||
|
sb.append(String.format("│ %2d │ %-10s │ %10d │ %8d │ %10s │\n", |
||||
|
rank.getRank(), |
||||
|
truncateString(rank.getName(), 10), |
||||
|
rank.getCommitCount(), |
||||
|
rank.getRepoCount(), |
||||
|
stars)); |
||||
|
} |
||||
|
sb.append("└────┴────────────┴────────────┴──────────┴────────────┘\n\n"); |
||||
|
} |
||||
|
|
||||
|
// 仓库排行榜 |
||||
|
if (data.getRepoRanks() != null && !data.getRepoRanks().isEmpty()) { |
||||
|
sb.append("🏆 仓库活跃度排行榜 (按提交次数):\n"); |
||||
|
sb.append("┌────┬──────────────────────┬────────────┬──────────┬──────────┐\n"); |
||||
|
sb.append("│排名│ 仓库名称 │ 提交次数 │ 开发者数 │ 活跃度 │\n"); |
||||
|
sb.append("├────┼──────────────────────┼────────────┼──────────┼──────────┤\n"); |
||||
|
|
||||
|
int displayCount = Math.min(10, data.getRepoRanks().size()); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
RepoRank rank = data.getRepoRanks().get(i); |
||||
|
String stars = getStars(rank.getCommitCount(), data.getBasicInfo().getTotalCommits()); |
||||
|
sb.append(String.format("│ %2d │ %-20s │ %10d │ %8d │ %8s │\n", |
||||
|
rank.getRank(), |
||||
|
truncateString(rank.getDisplayName(), 20), |
||||
|
rank.getCommitCount(), |
||||
|
rank.getDeveloperCount(), |
||||
|
stars)); |
||||
|
} |
||||
|
sb.append("└────┴──────────────────────┴────────────┴──────────┴──────────┘\n\n"); |
||||
|
} |
||||
|
|
||||
|
// 文件类型统计(新增) |
||||
|
if (data.getFileTypeStats() != null && !data.getFileTypeStats().isEmpty()) { |
||||
|
sb.append("📄 文件类型统计:\n"); |
||||
|
|
||||
|
int totalFiles = 0; |
||||
|
for (FileTypeStats stat : data.getFileTypeStats()) { |
||||
|
totalFiles += stat.getFileCount(); |
||||
|
} |
||||
|
|
||||
|
int displayCount = Math.min(8, data.getFileTypeStats().size()); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
FileTypeStats stat = data.getFileTypeStats().get(i); |
||||
|
|
||||
|
int barLength = totalFiles > 0 ? (int) (stat.getFileCount() * 40.0 / totalFiles) : 0; |
||||
|
String bar = "█".repeat(Math.max(0, barLength)); |
||||
|
double percentage = totalFiles > 0 ? (stat.getFileCount() * 100.0 / totalFiles) : 0; |
||||
|
sb.append(String.format(" %-12s %s %d个文件 (%.1f%%)\n", |
||||
|
stat.getFileType(), |
||||
|
bar, |
||||
|
stat.getFileCount(), |
||||
|
percentage)); |
||||
|
} |
||||
|
sb.append("\n"); |
||||
|
} |
||||
|
|
||||
|
// 多颗粒度汇总 |
||||
|
sb.append("📊 多颗粒度汇总:\n"); |
||||
|
|
||||
|
// 当颗粒度不是"week"且时间范围大于7天时,显示周分布 |
||||
|
if (!"week".equals(granularity) && daysBetween > 7) { |
||||
|
Map<String, Integer> weeklySummary = calculateWeeklySummary(commits, since, until); |
||||
|
if (!weeklySummary.isEmpty()) { |
||||
|
sb.append("├── 周分布: ").append(formatSummary(weeklySummary, "第{}周")).append("\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 当颗粒度不是"month"且时间范围大于30天时,显示月分布 |
||||
|
if (!"month".equals(granularity) && daysBetween > 30) { |
||||
|
Map<String, Integer> monthlySummary = calculateMonthlySummary(commits, since, until); |
||||
|
if (!monthlySummary.isEmpty()) { |
||||
|
sb.append("├── 月分布: ").append(formatSummary(monthlySummary, "{}月")).append("\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 当颗粒度不是"year"且时间范围大于365天时,显示年分布 |
||||
|
if (!"year".equals(granularity) && daysBetween > 365) { |
||||
|
Map<String, Integer> yearlySummary = calculateYearlySummary(commits, since, until); |
||||
|
if (!yearlySummary.isEmpty()) { |
||||
|
sb.append("├── 年分布: ").append(formatSummary(yearlySummary, "{}年")).append("\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 当颗粒度不是"day"且时间范围小于等于30天时,显示星期分布 |
||||
|
if (!"day".equals(granularity) && daysBetween <= 30) { |
||||
|
sb.append("├── 星期分布: "); |
||||
|
Map<String, Integer> dayMap = new LinkedHashMap<>(); |
||||
|
if (data.getDayStats() != null) { |
||||
|
for (DayStats dayStat : data.getDayStats()) { |
||||
|
dayMap.put(dayStat.getDayName(), dayStat.getCommitCount()); |
||||
|
} |
||||
|
sb.append(formatSummary(dayMap, "{}")).append("\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
sb.append("\n"); |
||||
|
|
||||
|
// 关键指标(根据颗粒度调整) |
||||
|
sb.append("📊 关键指标(按").append(granularity).append("):\n"); |
||||
|
|
||||
|
// 开发者指标 |
||||
|
if (data.getDeveloperRanks() != null && !data.getDeveloperRanks().isEmpty()) { |
||||
|
DeveloperRank topDev = data.getDeveloperRanks().get(0); |
||||
|
sb.append("├── 👑 最活跃开发者: ").append(topDev.getName()) |
||||
|
.append(" (").append(topDev.getCommitCount()).append("次提交)\n"); |
||||
|
} |
||||
|
|
||||
|
// 仓库指标 |
||||
|
if (data.getRepoRanks() != null && !data.getRepoRanks().isEmpty()) { |
||||
|
RepoRank topRepo = data.getRepoRanks().get(0); |
||||
|
sb.append("├── 🏆 最活跃仓库: ").append(topRepo.getDisplayName()) |
||||
|
.append(" (").append(topRepo.getCommitCount()).append("次提交)\n"); |
||||
|
} |
||||
|
|
||||
|
// 时间颗粒度指标 |
||||
|
switch (granularity) { |
||||
|
case "day": |
||||
|
// 按天的颗粒度,显示最活跃的一天 |
||||
|
Map<String, Integer> dailyStats = calculateDailySummary(commits, since, until); |
||||
|
String mostActiveDay = getMostActivePeriod(dailyStats); |
||||
|
if (mostActiveDay != null) { |
||||
|
sb.append("├── 📅 最活跃日期: ").append(mostActiveDay) |
||||
|
.append(" (").append(dailyStats.get(mostActiveDay)).append("次提交)\n"); |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case "week": |
||||
|
// 按周的颗粒度,显示最活跃的一周 |
||||
|
Map<String, Integer> weeklyStats = calculateWeeklySummary(commits, since, until); |
||||
|
String mostActiveWeek = getMostActivePeriod(weeklyStats); |
||||
|
if (mostActiveWeek != null) { |
||||
|
sb.append("├── 📅 最活跃周: ").append(mostActiveWeek) |
||||
|
.append(" (").append(weeklyStats.get(mostActiveWeek)).append("次提交)\n"); |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case "month": |
||||
|
// 按月的颗粒度,显示最活跃的月份 |
||||
|
Map<String, Integer> monthlyStats = calculateMonthlySummary(commits, since, until); |
||||
|
String mostActiveMonth = getMostActivePeriod(monthlyStats); |
||||
|
if (mostActiveMonth != null) { |
||||
|
sb.append("├── 📅 最活跃月份: ").append(mostActiveMonth) |
||||
|
.append(" (").append(monthlyStats.get(mostActiveMonth)).append("次提交)\n"); |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case "year": |
||||
|
// 按年的颗粒度,显示最活跃的年份 |
||||
|
Map<String, Integer> yearlyStats = calculateYearlySummary(commits, since, until); |
||||
|
String mostActiveYear = getMostActivePeriod(yearlyStats); |
||||
|
if (mostActiveYear != null) { |
||||
|
sb.append("├── 📅 最活跃年份: ").append(mostActiveYear) |
||||
|
.append(" (").append(yearlyStats.get(mostActiveYear)).append("次提交)\n"); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// 团队活跃度指标 |
||||
|
double avgDailyCommits = data.getBasicInfo().getTotalCommits() * 1.0 / Math.max(1, daysBetween); |
||||
|
sb.append("└── ⚡ 团队活跃度: ").append(getActivityLevel(avgDailyCommits)) |
||||
|
.append("\n\n"); |
||||
|
|
||||
|
sb.append("🕐 报告生成时间: ").append(data.getGeneratedTime()).append("\n"); |
||||
|
sb.append("======================================================================\n"); |
||||
|
|
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取最活跃的时间段 |
||||
|
*/ |
||||
|
private String getMostActivePeriod(Map<String, Integer> periodStats) { |
||||
|
if (periodStats == null || periodStats.isEmpty()) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return periodStats.entrySet().stream() |
||||
|
.max(Map.Entry.comparingByValue()) |
||||
|
.map(Map.Entry::getKey) |
||||
|
.orElse(null); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算日汇总 |
||||
|
*/ |
||||
|
private Map<String, Integer> calculateDailySummary(List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> dailyStats = new TreeMap<>(); |
||||
|
|
||||
|
for (CommitDetail commit : commits) { |
||||
|
if (commit.getCommitTime() != null) { |
||||
|
LocalDate date = commit.getCommitTime().toLocalDate(); |
||||
|
String dateKey = date.format(DATE_FORMATTER); |
||||
|
dailyStats.merge(dateKey, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return dailyStats; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成日分布 |
||||
|
*/ |
||||
|
private void generateDailyDistribution(StringBuilder sb, List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> dailyStats = calculateDailySummary(commits, since, until); |
||||
|
generateDistributionChart(sb, dailyStats, "{}"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成周分布 |
||||
|
*/ |
||||
|
private void generateWeeklyDistribution(StringBuilder sb, List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> weeklyStats = calculateWeeklySummary(commits, since, until); |
||||
|
generateDistributionChart(sb, weeklyStats, "第{}周"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成月分布 |
||||
|
*/ |
||||
|
private void generateMonthlyDistribution(StringBuilder sb, List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> monthlyStats = calculateMonthlySummary(commits, since, until); |
||||
|
generateDistributionChart(sb, monthlyStats, "{}月"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成年分布 |
||||
|
*/ |
||||
|
private void generateYearlyDistribution(StringBuilder sb, List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> yearlyStats = calculateYearlySummary(commits, since, until); |
||||
|
generateDistributionChart(sb, yearlyStats, "{}年"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成分布图表 |
||||
|
*/ |
||||
|
private void generateDistributionChart(StringBuilder sb, Map<String, Integer> stats, String labelFormat) { |
||||
|
if (stats == null || stats.isEmpty()) { |
||||
|
sb.append(" 无数据\n"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int maxCount = stats.values().stream().mapToInt(Integer::intValue).max().orElse(1); |
||||
|
|
||||
|
for (Map.Entry<String, Integer> entry : stats.entrySet()) { |
||||
|
String label = labelFormat.replace("{}", entry.getKey()); |
||||
|
int barLength = (int) (entry.getValue() * 40.0 / maxCount); |
||||
|
String bar = "█".repeat(Math.max(0, barLength)); |
||||
|
sb.append(String.format(" %-12s %s %d次\n", label, bar, entry.getValue())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算周汇总 |
||||
|
*/ |
||||
|
private Map<String, Integer> calculateWeeklySummary(List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> weeklyStats = new TreeMap<>(); |
||||
|
|
||||
|
for (CommitDetail commit : commits) { |
||||
|
if (commit.getCommitTime() != null) { |
||||
|
LocalDate date = commit.getCommitTime().toLocalDate(); |
||||
|
// 使用ISO周数 |
||||
|
int weekNumber = date.get(WeekFields.ISO.weekOfYear()); |
||||
|
String weekKey = String.format("%d-%02d", date.getYear(), weekNumber); |
||||
|
weeklyStats.merge(weekKey, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return weeklyStats; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算月汇总 |
||||
|
*/ |
||||
|
private Map<String, Integer> calculateMonthlySummary(List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> monthlyStats = new TreeMap<>(); |
||||
|
|
||||
|
for (CommitDetail commit : commits) { |
||||
|
if (commit.getCommitTime() != null) { |
||||
|
LocalDate date = commit.getCommitTime().toLocalDate(); |
||||
|
String monthKey = date.format(MONTH_FORMATTER); |
||||
|
monthlyStats.merge(monthKey, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return monthlyStats; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算年汇总 |
||||
|
*/ |
||||
|
private Map<String, Integer> calculateYearlySummary(List<CommitDetail> commits, String since, String until) { |
||||
|
Map<String, Integer> yearlyStats = new TreeMap<>(); |
||||
|
|
||||
|
for (CommitDetail commit : commits) { |
||||
|
if (commit.getCommitTime() != null) { |
||||
|
LocalDate date = commit.getCommitTime().toLocalDate(); |
||||
|
String yearKey = date.format(YEAR_FORMATTER); |
||||
|
yearlyStats.merge(yearKey, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return yearlyStats; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 格式化汇总信息 |
||||
|
*/ |
||||
|
private String formatSummary(Map<String, Integer> summary, String labelFormat) { |
||||
|
if (summary == null || summary.isEmpty()) { |
||||
|
return "无数据"; |
||||
|
} |
||||
|
|
||||
|
List<String> parts = new ArrayList<>(); |
||||
|
for (Map.Entry<String, Integer> entry : summary.entrySet()) { |
||||
|
String label = labelFormat.replace("{}", entry.getKey()); |
||||
|
parts.add(label + ":" + entry.getValue()); |
||||
|
} |
||||
|
return String.join(" | ", parts); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算天数差 |
||||
|
*/ |
||||
|
private long calculateDaysBetween(String since, String until) { |
||||
|
try { |
||||
|
LocalDate start = LocalDate.parse(since, DATE_FORMATTER); |
||||
|
LocalDate end = LocalDate.parse(until, DATE_FORMATTER); |
||||
|
return ChronoUnit.DAYS.between(start, end) + 1; |
||||
|
} catch (Exception e) { |
||||
|
return 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成星标 |
||||
|
*/ |
||||
|
private String getStars(int count, int total) { |
||||
|
if (total == 0) return "☆☆☆☆☆"; |
||||
|
|
||||
|
double ratio = count * 1.0 / total; |
||||
|
if (ratio > 0.15) return "⭐⭐⭐⭐⭐"; |
||||
|
else if (ratio > 0.10) return "⭐⭐⭐⭐☆"; |
||||
|
else if (ratio > 0.05) return "⭐⭐⭐☆☆"; |
||||
|
else if (ratio > 0.02) return "⭐⭐☆☆☆"; |
||||
|
else return "⭐☆☆☆☆"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 截断字符串 |
||||
|
*/ |
||||
|
private String truncateString(String str, int maxLength) { |
||||
|
if (str == null) return ""; |
||||
|
if (str.length() <= maxLength) return str; |
||||
|
return str.substring(0, maxLength - 2) + ".."; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断活跃度等级 |
||||
|
*/ |
||||
|
private String getActivityLevel(double avgDailyCommits) { |
||||
|
if (avgDailyCommits > 50) return "极高"; |
||||
|
else if (avgDailyCommits > 30) return "高"; |
||||
|
else if (avgDailyCommits > 15) return "中等偏上"; |
||||
|
else if (avgDailyCommits > 8) return "中等"; |
||||
|
else if (avgDailyCommits > 3) return "中等偏下"; |
||||
|
else return "低"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 提交详情类(内部使用) |
||||
|
*/ |
||||
|
private static class CommitDetail { |
||||
|
private String author; |
||||
|
private String repoName; |
||||
|
private ZonedDateTime commitTime; |
||||
|
private String sha; |
||||
|
private String message; |
||||
|
private String filesJson; // 新增字段 |
||||
|
|
||||
|
// Getters and Setters |
||||
|
public String getAuthor() { return author; } |
||||
|
public void setAuthor(String author) { this.author = author; } |
||||
|
|
||||
|
public String getRepoName() { return repoName; } |
||||
|
public void setRepoName(String repoName) { this.repoName = repoName; } |
||||
|
|
||||
|
public ZonedDateTime getCommitTime() { return commitTime; } |
||||
|
public void setCommitTime(ZonedDateTime commitTime) { this.commitTime = commitTime; } |
||||
|
|
||||
|
public String getSha() { return sha; } |
||||
|
public void setSha(String sha) { this.sha = sha; } |
||||
|
|
||||
|
public String getMessage() { return message; } |
||||
|
public void setMessage(String message) { this.message = message; } |
||||
|
|
||||
|
public String getFilesJson() { return filesJson; } |
||||
|
public void setFilesJson(String filesJson) { this.filesJson = filesJson; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,802 @@ |
|||||
|
package com.chenhai.chenhaiai.service.gitNew; |
||||
|
|
||||
|
import com.chenhai.chenhaiai.entity.git.*; |
||||
|
import com.chenhai.common.core.redis.RedisCache; |
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.time.*; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.*; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class GiteaQueryService { |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisCache redisCache; |
||||
|
|
||||
|
private final ObjectMapper objectMapper; |
||||
|
|
||||
|
// Redis Key常量 |
||||
|
private static final String REPO_LIST_KEY = "gitea:repos:list"; |
||||
|
private static final String REPO_INFO_KEY_PREFIX = "gitea:repo:"; |
||||
|
private static final String COMMIT_KEY_PREFIX = "gitea:commit:"; |
||||
|
private static final String COMMITS_BY_DATE_KEY_PREFIX = "gitea:commits:by_date:"; |
||||
|
|
||||
|
// 时间格式化器 - 只使用年月日格式 |
||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
||||
|
|
||||
|
public GiteaQueryService() { |
||||
|
this.objectMapper = new ObjectMapper(); |
||||
|
this.objectMapper.registerModule(new JavaTimeModule()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 主查询方法:分析指定时间范围的Git数据 |
||||
|
*/ |
||||
|
public GitAnalysisData analyzeGitDataFromRedis(String since, String until) { |
||||
|
String taskId = UUID.randomUUID().toString().substring(0, 8); |
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
|
||||
|
log.info("开始Redis查询分析任务[{}]: {} 至 {}", taskId, since, until); |
||||
|
|
||||
|
try { |
||||
|
// 解析时间范围 - 使用年月日格式 |
||||
|
LocalDate sinceDate = parseDateString(since); |
||||
|
LocalDate untilDate = parseDateString(until); |
||||
|
|
||||
|
// 转换为一天的开始和结束时间 |
||||
|
ZonedDateTime sinceTime = sinceDate.atStartOfDay(ZoneId.systemDefault()); |
||||
|
ZonedDateTime untilTime = untilDate.atTime(23, 59, 59).atZone(ZoneId.systemDefault()); |
||||
|
|
||||
|
// 验证时间范围有效性 |
||||
|
if (sinceTime.isAfter(untilTime)) { |
||||
|
log.warn("任务[{}] 时间范围无效: sinceTime={}, untilTime={}", taskId, sinceTime, untilTime); |
||||
|
return buildEmptyGitAnalysisData("时间范围无效: 开始时间不能晚于结束时间"); |
||||
|
} |
||||
|
|
||||
|
// 1. 获取所有仓库 |
||||
|
Map<String, String> allRepos = redisCache.getCacheMap(REPO_LIST_KEY); |
||||
|
if (allRepos == null || allRepos.isEmpty()) { |
||||
|
log.warn("任务[{}] Redis中无仓库数据", taskId); |
||||
|
return buildEmptyGitAnalysisData("Redis中无仓库数据"); |
||||
|
} |
||||
|
|
||||
|
int totalRepos = allRepos.size(); |
||||
|
log.info("任务[{}] Redis中发现仓库: {} 个", taskId, totalRepos); |
||||
|
|
||||
|
// 2. 筛选活跃仓库 |
||||
|
List<String> activeRepoNames = new ArrayList<>(); |
||||
|
Map<String, Integer> repoCommitCounts = new HashMap<>(); |
||||
|
Map<String, List<CommitInfo>> repoCommitsMap = new HashMap<>(); |
||||
|
|
||||
|
for (Map.Entry<String, String> entry : allRepos.entrySet()) { |
||||
|
String repoFullPath = entry.getValue(); |
||||
|
|
||||
|
// 获取时间范围内的提交 |
||||
|
List<CommitInfo> commits = getCommitsInRange(repoFullPath, sinceTime, untilTime); |
||||
|
if (!commits.isEmpty()) { |
||||
|
activeRepoNames.add(repoFullPath); |
||||
|
repoCommitCounts.put(repoFullPath, commits.size()); |
||||
|
repoCommitsMap.put(repoFullPath, commits); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int activeRepoCount = activeRepoNames.size(); |
||||
|
log.info("任务[{}] 活跃仓库: {} 个", taskId, activeRepoCount); |
||||
|
|
||||
|
if (activeRepoCount == 0) { |
||||
|
return buildSimpleGitAnalysisData(since, until, totalRepos, startTime); |
||||
|
} |
||||
|
|
||||
|
// 3. 详细分析活跃仓库 |
||||
|
DetailedAnalysisResult detailResult = analyzeActiveRepositories( |
||||
|
activeRepoNames, repoCommitsMap, sinceTime, untilTime, taskId); |
||||
|
|
||||
|
// 4. 构建返回数据 |
||||
|
long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
return buildGitAnalysisData(since, until, totalRepos, activeRepoCount, |
||||
|
detailResult, analysisTime); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("Redis查询分析任务[{}]失败: {}", taskId, e.getMessage(), e); |
||||
|
throw new RuntimeException("Git分析失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析日期字符串 (yyyy-MM-dd) |
||||
|
*/ |
||||
|
private LocalDate parseDateString(String dateStr) { |
||||
|
if (dateStr == null || dateStr.isEmpty()) { |
||||
|
throw new IllegalArgumentException("日期字符串不能为空"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// 只解析年月日 |
||||
|
return LocalDate.parse(dateStr, DATE_FORMATTER); |
||||
|
} catch (Exception e) { |
||||
|
log.error("日期格式解析失败: {}, 请使用 yyyy-MM-dd 格式", dateStr); |
||||
|
throw new IllegalArgumentException("日期格式错误: " + dateStr + ",请使用 yyyy-MM-dd 格式", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取时间范围内的提交 |
||||
|
*/ |
||||
|
private List<CommitInfo> getCommitsInRange(String repoFullPath, |
||||
|
ZonedDateTime since, |
||||
|
ZonedDateTime until) { |
||||
|
try { |
||||
|
// 使用日期索引遍历 |
||||
|
return getCommitsByDateIndex(repoFullPath, since, until); |
||||
|
} catch (Exception e) { |
||||
|
log.warn("获取仓库 {} 提交失败: {}", repoFullPath, e.getMessage()); |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 按日期索引获取提交 |
||||
|
*/ |
||||
|
private List<CommitInfo> getCommitsByDateIndex(String repoFullPath, |
||||
|
ZonedDateTime since, |
||||
|
ZonedDateTime until) { |
||||
|
List<CommitInfo> result = new ArrayList<>(); |
||||
|
LocalDate startDate = since.toLocalDate(); |
||||
|
LocalDate endDate = until.toLocalDate(); |
||||
|
|
||||
|
LocalDate currentDate = startDate; |
||||
|
while (!currentDate.isAfter(endDate)) { |
||||
|
String dateKey = COMMITS_BY_DATE_KEY_PREFIX + repoFullPath + ":" + currentDate; |
||||
|
Set<String> dateCommits = redisCache.getCacheSet(dateKey); |
||||
|
|
||||
|
if (dateCommits != null && !dateCommits.isEmpty()) { |
||||
|
for (String sha : dateCommits) { |
||||
|
try { |
||||
|
String commitKey = COMMIT_KEY_PREFIX + repoFullPath + ":" + sha; |
||||
|
Map<String, Object> commitData = redisCache.getCacheMap(commitKey); |
||||
|
|
||||
|
if (commitData != null && isValidCommit(commitData, since, until)) { |
||||
|
CommitInfo commitInfo = convertToCommitInfo(commitData); |
||||
|
result.add(commitInfo); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.debug("处理提交 {} 失败: {}", sha, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
currentDate = currentDate.plusDays(1); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查提交是否在时间范围内 |
||||
|
*/ |
||||
|
private boolean isValidCommit(Map<String, Object> commitData, |
||||
|
ZonedDateTime since, |
||||
|
ZonedDateTime until) { |
||||
|
if (commitData.get("timestamp") == null) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
long timestamp = ((Number) commitData.get("timestamp")).longValue(); |
||||
|
ZonedDateTime commitTime = ZonedDateTime.ofInstant( |
||||
|
Instant.ofEpochSecond(timestamp), |
||||
|
ZoneId.systemDefault() |
||||
|
); |
||||
|
|
||||
|
return !commitTime.isBefore(since) && !commitTime.isAfter(until); |
||||
|
} catch (ClassCastException e) { |
||||
|
log.warn("提交时间戳格式错误: {}", commitData.get("timestamp")); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 转换提交数据 |
||||
|
*/ |
||||
|
private CommitInfo convertToCommitInfo(Map<String, Object> commitData) { |
||||
|
CommitInfo info = new CommitInfo(); |
||||
|
|
||||
|
try { |
||||
|
info.sha = commitData.get("sha") != null ? commitData.get("sha").toString() : null; |
||||
|
info.author = commitData.get("author") != null ? commitData.get("author").toString() : null; |
||||
|
info.message = commitData.get("message") != null ? commitData.get("message").toString() : null; |
||||
|
info.timeStr = commitData.get("time_str") != null ? commitData.get("time_str").toString() : null; |
||||
|
info.filesJson = commitData.get("files_json") != null ? commitData.get("files_json").toString() : null; |
||||
|
|
||||
|
if (commitData.get("timestamp") != null) { |
||||
|
try { |
||||
|
info.timestamp = ((Number) commitData.get("timestamp")).longValue(); |
||||
|
info.commitTime = ZonedDateTime.ofInstant( |
||||
|
Instant.ofEpochSecond(info.timestamp), |
||||
|
ZoneId.systemDefault() |
||||
|
); |
||||
|
} catch (ClassCastException e) { |
||||
|
log.warn("时间戳格式错误: {}", commitData.get("timestamp")); |
||||
|
info.timestamp = null; |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.warn("转换提交数据失败: {}", e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
return info; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 详细分析活跃仓库 |
||||
|
*/ |
||||
|
private DetailedAnalysisResult analyzeActiveRepositories(List<String> activeRepoNames, |
||||
|
Map<String, List<CommitInfo>> repoCommitsMap, |
||||
|
ZonedDateTime sinceTime, |
||||
|
ZonedDateTime untilTime, |
||||
|
String taskId) { |
||||
|
Map<String, DeveloperData> devDataMap = new HashMap<>(); |
||||
|
Map<String, RepoData> repoDataMap = new HashMap<>(); |
||||
|
Map<DayOfWeek, Integer> dayStats = new HashMap<>(); |
||||
|
Map<Integer, Integer> hourStats = new HashMap<>(); |
||||
|
Map<String, Integer> fileTypeStats = new HashMap<>(); |
||||
|
|
||||
|
int totalCommits = 0; |
||||
|
|
||||
|
log.info("任务[{}] 开始详细分析 {} 个活跃仓库", taskId, activeRepoNames.size()); |
||||
|
|
||||
|
for (String repoFullPath : activeRepoNames) { |
||||
|
try { |
||||
|
List<CommitInfo> commits = repoCommitsMap.get(repoFullPath); |
||||
|
if (commits == null || commits.isEmpty()) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 获取仓库信息 |
||||
|
String repoKey = REPO_INFO_KEY_PREFIX + repoFullPath; |
||||
|
Map<String, Object> repoInfo = redisCache.getCacheMap(repoKey); |
||||
|
|
||||
|
RepoData repoData = new RepoData(); |
||||
|
repoData.repoName = repoFullPath; |
||||
|
repoData.displayName = repoInfo != null && repoInfo.get("name") != null |
||||
|
? repoInfo.get("name").toString() |
||||
|
: repoFullPath; |
||||
|
|
||||
|
// 分析该仓库的所有提交 |
||||
|
for (CommitInfo commit : commits) { |
||||
|
if (commit.author == null || commit.author.isEmpty()) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
DeveloperData devData = devDataMap.computeIfAbsent(commit.author, |
||||
|
k -> new DeveloperData(commit.author)); |
||||
|
devData.commitCount++; |
||||
|
devData.repos.add(repoFullPath); |
||||
|
|
||||
|
repoData.commitCount++; |
||||
|
repoData.developers.add(commit.author); |
||||
|
|
||||
|
if (commit.commitTime != null) { |
||||
|
DayOfWeek day = commit.commitTime.getDayOfWeek(); |
||||
|
int hour = commit.commitTime.getHour(); |
||||
|
dayStats.merge(day, 1, Integer::sum); |
||||
|
hourStats.merge(hour, 1, Integer::sum); |
||||
|
} |
||||
|
|
||||
|
if (commit.filesJson != null && !commit.filesJson.isEmpty()) { |
||||
|
try { |
||||
|
List<Map<String, String>> files = objectMapper.readValue( |
||||
|
commit.filesJson, new TypeReference<List<Map<String, String>>>() {}); |
||||
|
|
||||
|
for (Map<String, String> file : files) { |
||||
|
String filename = file.get("filename"); |
||||
|
if (filename != null) { |
||||
|
String fileType = getFileType(filename); |
||||
|
fileTypeStats.merge(fileType, 1, Integer::sum); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.debug("解析文件JSON失败: {}", e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
totalCommits++; |
||||
|
} |
||||
|
|
||||
|
if (repoData.commitCount > 0) { |
||||
|
repoDataMap.put(repoFullPath, repoData); |
||||
|
} |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.debug("任务[{}] 仓库 {} 分析失败: {}", taskId, repoFullPath, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
log.info("任务[{}] 详细分析完成,总提交: {}", taskId, totalCommits); |
||||
|
|
||||
|
return new DetailedAnalysisResult(devDataMap, repoDataMap, dayStats, hourStats, |
||||
|
fileTypeStats, totalCommits); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取文件类型 |
||||
|
*/ |
||||
|
private String getFileType(String filename) { |
||||
|
if (filename == null || filename.isEmpty()) { |
||||
|
return "未知"; |
||||
|
} |
||||
|
|
||||
|
int dotIndex = filename.lastIndexOf('.'); |
||||
|
if (dotIndex > 0 && dotIndex < filename.length() - 1) { |
||||
|
String ext = filename.substring(dotIndex + 1).toLowerCase(); |
||||
|
|
||||
|
Map<String, String> typeMap = new HashMap<>(); |
||||
|
typeMap.put("java", "Java"); |
||||
|
typeMap.put("py", "Python"); |
||||
|
typeMap.put("js", "JavaScript"); |
||||
|
typeMap.put("ts", "TypeScript"); |
||||
|
typeMap.put("vue", "Vue"); |
||||
|
typeMap.put("html", "HTML"); |
||||
|
typeMap.put("css", "CSS"); |
||||
|
typeMap.put("md", "Markdown"); |
||||
|
typeMap.put("json", "JSON"); |
||||
|
typeMap.put("yml", "YAML"); |
||||
|
typeMap.put("yaml", "YAML"); |
||||
|
typeMap.put("xml", "XML"); |
||||
|
typeMap.put("sql", "SQL"); |
||||
|
typeMap.put("sh", "Shell"); |
||||
|
|
||||
|
return typeMap.getOrDefault(ext, ext.toUpperCase()); |
||||
|
} |
||||
|
|
||||
|
return "无扩展名"; |
||||
|
} |
||||
|
|
||||
|
// ==================== 数据结构类 ==================== |
||||
|
|
||||
|
private static class CommitInfo { |
||||
|
String sha; |
||||
|
String author; |
||||
|
String message; |
||||
|
Long timestamp; |
||||
|
String timeStr; |
||||
|
String filesJson; |
||||
|
ZonedDateTime commitTime; |
||||
|
} |
||||
|
|
||||
|
private static class DeveloperData { |
||||
|
String name; |
||||
|
int commitCount = 0; |
||||
|
Set<String> repos = new HashSet<>(); |
||||
|
|
||||
|
DeveloperData(String name) { |
||||
|
this.name = name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static class RepoData { |
||||
|
String repoName; |
||||
|
String displayName; |
||||
|
int commitCount = 0; |
||||
|
Set<String> developers = new HashSet<>(); |
||||
|
} |
||||
|
|
||||
|
private static class DetailedAnalysisResult { |
||||
|
private final Map<String, DeveloperData> devDataMap; |
||||
|
private final Map<String, RepoData> repoDataMap; |
||||
|
private final Map<DayOfWeek, Integer> dayStats; |
||||
|
private final Map<Integer, Integer> hourStats; |
||||
|
private final Map<String, Integer> fileTypeStats; |
||||
|
private final int totalCommits; |
||||
|
|
||||
|
public DetailedAnalysisResult(Map<String, DeveloperData> devDataMap, |
||||
|
Map<String, RepoData> repoDataMap, |
||||
|
Map<DayOfWeek, Integer> dayStats, |
||||
|
Map<Integer, Integer> hourStats, |
||||
|
Map<String, Integer> fileTypeStats, |
||||
|
int totalCommits) { |
||||
|
this.devDataMap = devDataMap; |
||||
|
this.repoDataMap = repoDataMap; |
||||
|
this.dayStats = dayStats; |
||||
|
this.hourStats = hourStats; |
||||
|
this.fileTypeStats = fileTypeStats; |
||||
|
this.totalCommits = totalCommits; |
||||
|
} |
||||
|
|
||||
|
public Map<String, DeveloperData> getDevDataMap() { return devDataMap; } |
||||
|
public Map<String, RepoData> getRepoDataMap() { return repoDataMap; } |
||||
|
public Map<DayOfWeek, Integer> getDayStats() { return dayStats; } |
||||
|
public Map<Integer, Integer> getHourStats() { return hourStats; } |
||||
|
public Map<String, Integer> getFileTypeStats() { return fileTypeStats; } |
||||
|
public int getTotalCommits() { return totalCommits; } |
||||
|
} |
||||
|
|
||||
|
// ==================== 数据构建方法 ==================== |
||||
|
|
||||
|
private GitAnalysisData buildGitAnalysisData(String since, String until, |
||||
|
int totalRepos, int activeRepos, |
||||
|
DetailedAnalysisResult detailResult, |
||||
|
long analysisTime) { |
||||
|
GitAnalysisData data = new GitAnalysisData(); |
||||
|
|
||||
|
// 基础信息 - 使用格式化后的时间显示 |
||||
|
data.setBasicInfo(new BasicInfo( |
||||
|
formatDateForDisplay(since) + " 至 " + formatDateForDisplay(until), |
||||
|
totalRepos, |
||||
|
activeRepos, |
||||
|
detailResult.getDevDataMap().size(), |
||||
|
detailResult.getTotalCommits(), |
||||
|
analysisTime, |
||||
|
"Redis缓存查询" |
||||
|
)); |
||||
|
|
||||
|
buildDeveloperRanks(data, detailResult.getDevDataMap()); |
||||
|
buildRepoRanks(data, detailResult.getRepoDataMap()); |
||||
|
buildTimeDistribution(data, detailResult.getDayStats()); |
||||
|
buildFileTypeStats(data, detailResult.getFileTypeStats()); |
||||
|
|
||||
|
// 修改这里:只显示年月日 |
||||
|
data.setGeneratedTime(LocalDate.now().format(DATE_FORMATTER)); |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
private void buildDeveloperRanks(GitAnalysisData data, Map<String, DeveloperData> devDataMap) { |
||||
|
if (devDataMap != null && !devDataMap.isEmpty()) { |
||||
|
List<DeveloperData> devList = new ArrayList<>(devDataMap.values()); |
||||
|
devList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
|
||||
|
List<DeveloperRank> developerRanks = new ArrayList<>(); |
||||
|
int rank = 1; |
||||
|
for (DeveloperData dev : devList) { |
||||
|
if (rank > 15) break; |
||||
|
developerRanks.add(new DeveloperRank(rank++, dev.name, dev.commitCount, dev.repos.size())); |
||||
|
} |
||||
|
data.setDeveloperRanks(developerRanks); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void buildRepoRanks(GitAnalysisData data, Map<String, RepoData> repoDataMap) { |
||||
|
if (repoDataMap != null && !repoDataMap.isEmpty()) { |
||||
|
List<RepoData> repoList = new ArrayList<>(repoDataMap.values()); |
||||
|
repoList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); |
||||
|
|
||||
|
List<RepoRank> repoRanks = new ArrayList<>(); |
||||
|
int rank = 1; |
||||
|
for (RepoData repo : repoList) { |
||||
|
if (rank > 15) break; |
||||
|
repoRanks.add(new RepoRank(rank++, repo.repoName, repo.displayName, |
||||
|
repo.commitCount, repo.developers.size())); |
||||
|
} |
||||
|
data.setRepoRanks(repoRanks); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void buildTimeDistribution(GitAnalysisData data, Map<DayOfWeek, Integer> dayOfWeekStats) { |
||||
|
if (dayOfWeekStats != null && !dayOfWeekStats.isEmpty()) { |
||||
|
List<DayStats> statsList = new ArrayList<>(); |
||||
|
String[] dayNames = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; |
||||
|
DayOfWeek[] days = DayOfWeek.values(); |
||||
|
|
||||
|
for (int i = 0; i < 7; i++) { |
||||
|
int count = dayOfWeekStats.getOrDefault(days[i], 0); |
||||
|
statsList.add(new DayStats(dayNames[i], count)); |
||||
|
} |
||||
|
data.setDayStats(statsList); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void buildFileTypeStats(GitAnalysisData data, Map<String, Integer> fileTypeStats) { |
||||
|
if (fileTypeStats != null && !fileTypeStats.isEmpty()) { |
||||
|
List<Map.Entry<String, Integer>> fileList = new ArrayList<>(fileTypeStats.entrySet()); |
||||
|
fileList.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); |
||||
|
|
||||
|
List<FileTypeStats> fileTypeStatsList = new ArrayList<>(); |
||||
|
int count = 0; |
||||
|
for (Map.Entry<String, Integer> entry : fileList) { |
||||
|
if (count++ >= 15) break; |
||||
|
fileTypeStatsList.add(new FileTypeStats(entry.getKey(), entry.getValue())); |
||||
|
} |
||||
|
data.setFileTypeStats(fileTypeStatsList); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private GitAnalysisData buildEmptyGitAnalysisData(String message) { |
||||
|
GitAnalysisData data = new GitAnalysisData(); |
||||
|
data.setBasicInfo(new BasicInfo( |
||||
|
"无时间范围", |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
message |
||||
|
)); |
||||
|
data.setGeneratedTime(LocalDate.now().format(DATE_FORMATTER)); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
private GitAnalysisData buildSimpleGitAnalysisData(String since, String until, int totalRepos, |
||||
|
long startTime) { |
||||
|
long analysisTime = System.currentTimeMillis() - startTime; |
||||
|
|
||||
|
GitAnalysisData data = new GitAnalysisData(); |
||||
|
data.setBasicInfo(new BasicInfo( |
||||
|
formatDateForDisplay(since) + " 至 " + formatDateForDisplay(until), |
||||
|
totalRepos, |
||||
|
0, |
||||
|
0, |
||||
|
0, |
||||
|
analysisTime, |
||||
|
"无活跃仓库" |
||||
|
)); |
||||
|
data.setGeneratedTime(LocalDate.now().format(DATE_FORMATTER)); |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 格式化日期显示 |
||||
|
*/ |
||||
|
private String formatDateForDisplay(String dateStr) { |
||||
|
try { |
||||
|
LocalDate date = parseDateString(dateStr); |
||||
|
return date.format(DATE_FORMATTER); |
||||
|
} catch (Exception e) { |
||||
|
return dateStr; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取文本格式的分析报告(用于Vue2页面直接显示) |
||||
|
*/ |
||||
|
public Map<String, Object> getTextAnalysisReport(String since, String until) { |
||||
|
Map<String, Object> result = new HashMap<>(); |
||||
|
|
||||
|
try { |
||||
|
GitAnalysisData data = analyzeGitDataFromRedis(since, until); |
||||
|
String textReport = generateTextReport(data, since, until); |
||||
|
|
||||
|
result.put("success", true); |
||||
|
result.put("report", textReport); |
||||
|
result.put("rawData", data); |
||||
|
result.put("generatedTime", LocalDate.now().format(DATE_FORMATTER)); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
result.put("success", false); |
||||
|
result.put("error", e.getMessage()); |
||||
|
result.put("report", "生成报告失败: " + e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成文本格式的报告 |
||||
|
*/ |
||||
|
private String generateTextReport(GitAnalysisData data, String since, String until) { |
||||
|
StringBuilder sb = new StringBuilder(); |
||||
|
|
||||
|
String readableSince = formatDateForDisplay(since); |
||||
|
String readableUntil = formatDateForDisplay(until); |
||||
|
|
||||
|
sb.append("======================================================================\n"); |
||||
|
sb.append(" Gitea代码提交分析报告\n"); |
||||
|
sb.append("======================================================================\n\n"); |
||||
|
|
||||
|
sb.append("📅 分析时间范围: ").append(readableSince).append(" 至 ").append(readableUntil).append("\n"); |
||||
|
sb.append("⏱️ 查询耗时: ").append(String.format("%.2f", data.getBasicInfo().getAnalysisTime() / 1000.0)).append("秒 | 📊 数据来源: Redis缓存\n\n"); |
||||
|
|
||||
|
sb.append("📊 总体概览:\n"); |
||||
|
sb.append("├── 📦 总仓库数: ").append(data.getBasicInfo().getTotalRepos()).append(" 个\n"); |
||||
|
|
||||
|
double activeRepoPercentage = data.getBasicInfo().getTotalRepos() > 0 ? |
||||
|
(data.getBasicInfo().getActiveRepos() * 100.0 / data.getBasicInfo().getTotalRepos()) : 0; |
||||
|
sb.append("├── ⭐ 活跃仓库: ").append(data.getBasicInfo().getActiveRepos()).append(" 个 (") |
||||
|
.append(String.format("%.1f", activeRepoPercentage)).append("%)\n"); |
||||
|
|
||||
|
sb.append("├── 👥 活跃开发者: ").append(data.getBasicInfo().getActiveDevelopers()).append(" 人\n"); |
||||
|
sb.append("├── 📝 总提交次数: ").append(data.getBasicInfo().getTotalCommits()).append(" 次\n"); |
||||
|
|
||||
|
long days = calculateDaysBetween(since, until); |
||||
|
double dailyCommits = days > 0 ? data.getBasicInfo().getTotalCommits() * 1.0 / days : 0; |
||||
|
sb.append("└── 📈 日均提交: ").append(String.format("%.0f", dailyCommits)).append(" 次\n\n"); |
||||
|
|
||||
|
if (data.getDeveloperRanks() != null && !data.getDeveloperRanks().isEmpty()) { |
||||
|
sb.append("🏆 开发者排行榜 (按提交次数):\n"); |
||||
|
sb.append("┌────┬────────────┬────────────┬──────────┬────────────┐\n"); |
||||
|
sb.append("│排名│ 开发者 │ 提交次数 │ 参与仓库 │ 活跃度 │\n"); |
||||
|
sb.append("├────┼────────────┼────────────┼──────────┼────────────┤\n"); |
||||
|
|
||||
|
int displayCount = Math.min(10, data.getDeveloperRanks().size()); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
DeveloperRank rank = data.getDeveloperRanks().get(i); |
||||
|
|
||||
|
String stars = getStars(rank.getCommitCount(), data.getBasicInfo().getTotalCommits()); |
||||
|
sb.append(String.format("│ %2d │ %-10s │ %10d │ %8d │ %10s │\n", |
||||
|
rank.getRank(), |
||||
|
truncateString(rank.getName(), 10), |
||||
|
rank.getCommitCount(), |
||||
|
rank.getRepoCount(), |
||||
|
stars)); |
||||
|
} |
||||
|
sb.append("└────┴────────────┴────────────┴──────────┴────────────┘\n\n"); |
||||
|
} |
||||
|
|
||||
|
if (data.getRepoRanks() != null && !data.getRepoRanks().isEmpty()) { |
||||
|
sb.append("🏆 仓库活跃度排行榜 (按提交次数):\n"); |
||||
|
sb.append("┌────┬──────────────────────┬────────────┬──────────┬──────────┐\n"); |
||||
|
sb.append("│排名│ 仓库名称 │ 提交次数 │ 开发者数 │ 活跃度 │\n"); |
||||
|
sb.append("├────┼──────────────────────┼────────────┼──────────┼──────────┤\n"); |
||||
|
|
||||
|
int displayCount = Math.min(10, data.getRepoRanks().size()); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
RepoRank rank = data.getRepoRanks().get(i); |
||||
|
|
||||
|
String stars = getStars(rank.getCommitCount(), data.getBasicInfo().getTotalCommits()); |
||||
|
sb.append(String.format("│ %2d │ %-20s │ %10d │ %8d │ %8s │\n", |
||||
|
rank.getRank(), |
||||
|
truncateString(rank.getDisplayName(), 20), |
||||
|
rank.getCommitCount(), |
||||
|
rank.getDeveloperCount(), |
||||
|
stars)); |
||||
|
} |
||||
|
sb.append("└────┴──────────────────────┴────────────┴──────────┴──────────┘\n\n"); |
||||
|
} |
||||
|
|
||||
|
if (data.getDayStats() != null && !data.getDayStats().isEmpty()) { |
||||
|
sb.append("📈 提交时间分布(星期):\n"); |
||||
|
|
||||
|
int maxCount = 1; |
||||
|
for (DayStats dayStat : data.getDayStats()) { |
||||
|
if (dayStat.getCommitCount() > maxCount) { |
||||
|
maxCount = dayStat.getCommitCount(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (DayStats dayStat : data.getDayStats()) { |
||||
|
int barLength = (int) (dayStat.getCommitCount() * 30.0 / maxCount); |
||||
|
String bar = "█".repeat(Math.max(0, barLength)); |
||||
|
sb.append(String.format("%s: %s %d次\n", |
||||
|
dayStat.getDayName(), |
||||
|
bar, |
||||
|
dayStat.getCommitCount())); |
||||
|
} |
||||
|
sb.append("\n"); |
||||
|
} |
||||
|
|
||||
|
if (data.getFileTypeStats() != null && !data.getFileTypeStats().isEmpty()) { |
||||
|
sb.append("📄 文件类型统计:\n"); |
||||
|
|
||||
|
int totalFiles = 0; |
||||
|
for (FileTypeStats stat : data.getFileTypeStats()) { |
||||
|
totalFiles += stat.getFileCount(); |
||||
|
} |
||||
|
|
||||
|
int displayCount = Math.min(8, data.getFileTypeStats().size()); |
||||
|
for (int i = 0; i < displayCount; i++) { |
||||
|
FileTypeStats stat = data.getFileTypeStats().get(i); |
||||
|
|
||||
|
int barLength = totalFiles > 0 ? (int) (stat.getFileCount() * 40.0 / totalFiles) : 0; |
||||
|
String bar = "█".repeat(Math.max(0, barLength)); |
||||
|
double percentage = totalFiles > 0 ? (stat.getFileCount() * 100.0 / totalFiles) : 0; |
||||
|
sb.append(String.format("%-12s %s %d个文件 (%.1f%%)\n", |
||||
|
stat.getFileType(), |
||||
|
bar, |
||||
|
stat.getFileCount(), |
||||
|
percentage)); |
||||
|
} |
||||
|
sb.append("\n"); |
||||
|
} |
||||
|
|
||||
|
sb.append("📊 关键指标:\n"); |
||||
|
|
||||
|
if (data.getDayStats() != null && !data.getDayStats().isEmpty()) { |
||||
|
DayStats maxDay = null; |
||||
|
int maxCount = 0; |
||||
|
for (DayStats dayStat : data.getDayStats()) { |
||||
|
if (dayStat.getCommitCount() > maxCount) { |
||||
|
maxCount = dayStat.getCommitCount(); |
||||
|
maxDay = dayStat; |
||||
|
} |
||||
|
} |
||||
|
if (maxDay != null) { |
||||
|
sb.append("├── 📅 最活跃星期: ").append(maxDay.getDayName()) |
||||
|
.append(" (").append(maxCount).append("次提交)\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (data.getDeveloperRanks() != null && !data.getDeveloperRanks().isEmpty()) { |
||||
|
DeveloperRank topDev = data.getDeveloperRanks().get(0); |
||||
|
sb.append("├── 👑 最活跃开发者: ").append(topDev.getName()) |
||||
|
.append(" (").append(topDev.getCommitCount()).append("次提交,参与").append(topDev.getRepoCount()).append("个仓库)\n"); |
||||
|
} |
||||
|
|
||||
|
if (data.getRepoRanks() != null && !data.getRepoRanks().isEmpty()) { |
||||
|
RepoRank topRepo = data.getRepoRanks().get(0); |
||||
|
sb.append("├── 🏆 最活跃仓库: ").append(topRepo.getDisplayName()) |
||||
|
.append(" (").append(topRepo.getCommitCount()).append("次提交,").append(topRepo.getDeveloperCount()).append("个开发者)\n"); |
||||
|
} |
||||
|
|
||||
|
if (data.getDeveloperRanks() != null && !data.getDeveloperRanks().isEmpty()) { |
||||
|
int top3Commits = 0; |
||||
|
int limit = Math.min(3, data.getDeveloperRanks().size()); |
||||
|
for (int i = 0; i < limit; i++) { |
||||
|
DeveloperRank rank = data.getDeveloperRanks().get(i); |
||||
|
top3Commits += rank.getCommitCount(); |
||||
|
} |
||||
|
|
||||
|
int totalCommits = data.getBasicInfo().getTotalCommits(); |
||||
|
double top3Percentage = totalCommits > 0 ? (top3Commits * 100.0 / totalCommits) : 0; |
||||
|
sb.append("├── 👥 头部贡献: 前3名开发者贡献了 ").append(String.format("%.1f", top3Percentage)).append("% 的提交\n"); |
||||
|
} |
||||
|
|
||||
|
int totalDevs = data.getBasicInfo().getActiveDevelopers(); |
||||
|
int totalCommits = data.getBasicInfo().getTotalCommits(); |
||||
|
double avgCommitsPerDev = totalDevs > 0 ? (totalCommits * 1.0 / totalDevs) : 0; |
||||
|
sb.append("└── ⚡ 团队活跃度: ").append(getActivityLevel(avgCommitsPerDev)) |
||||
|
.append(" (平均每人").append(String.format("%.1f", avgCommitsPerDev)).append("次提交)\n\n"); |
||||
|
|
||||
|
String generatedTime = data.getGeneratedTime() != null ? data.getGeneratedTime() : |
||||
|
LocalDate.now().format(DATE_FORMATTER); |
||||
|
sb.append("🕐 报告生成时间: ").append(generatedTime).append("\n"); |
||||
|
sb.append("======================================================================\n"); |
||||
|
|
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算天数差 |
||||
|
*/ |
||||
|
private long calculateDaysBetween(String since, String until) { |
||||
|
try { |
||||
|
LocalDate start = parseDateString(since); |
||||
|
LocalDate end = parseDateString(until); |
||||
|
return java.time.temporal.ChronoUnit.DAYS.between(start, end) + 1; |
||||
|
} catch (Exception e) { |
||||
|
return 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成星标(活跃度指示) |
||||
|
*/ |
||||
|
private String getStars(int count, int total) { |
||||
|
if (total == 0) return "☆☆☆☆☆"; |
||||
|
|
||||
|
double ratio = count * 1.0 / total; |
||||
|
if (ratio > 0.15) return "⭐⭐⭐⭐⭐"; |
||||
|
else if (ratio > 0.10) return "⭐⭐⭐⭐☆"; |
||||
|
else if (ratio > 0.05) return "⭐⭐⭐☆☆"; |
||||
|
else if (ratio > 0.02) return "⭐⭐☆☆☆"; |
||||
|
else return "⭐☆☆☆☆"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 截断字符串 |
||||
|
*/ |
||||
|
private String truncateString(String str, int maxLength) { |
||||
|
if (str == null) return ""; |
||||
|
if (str.length() <= maxLength) return str; |
||||
|
return str.substring(0, maxLength - 2) + ".."; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断活跃度等级 |
||||
|
*/ |
||||
|
private String getActivityLevel(double avgCommits) { |
||||
|
if (avgCommits > 50) return "极高"; |
||||
|
else if (avgCommits > 30) return "高"; |
||||
|
else if (avgCommits > 15) return "中等偏上"; |
||||
|
else if (avgCommits > 8) return "中等"; |
||||
|
else if (avgCommits > 3) return "中等偏下"; |
||||
|
else return "低"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,209 @@ |
|||||
|
// CharacterStreamProcessor.java |
||||
|
package com.chenhai.chenhaiai.utils; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import org.springframework.ai.chat.client.ChatClient; |
||||
|
import reactor.core.publisher.Flux; |
||||
|
import reactor.core.publisher.FluxSink; |
||||
|
|
||||
|
import java.util.*; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
|
||||
|
/** |
||||
|
* 字符级流式输出处理器 |
||||
|
*/ |
||||
|
public class CharacterStreamProcessor { |
||||
|
|
||||
|
private static final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
// 字符输出配置 |
||||
|
private static final int CHARS_PER_CHUNK = 3; // 每次发送的字符数(越小越像打字效果) |
||||
|
private static final int MODULE_DELAY_MS = 300; // 模块间延迟 |
||||
|
private static final int LINE_BREAK_DELAY_MS = 100; // 换行延迟 |
||||
|
|
||||
|
/** |
||||
|
* 创建真正的字符级流式响应 |
||||
|
*/ |
||||
|
public static Flux<String> createCharacterStream( |
||||
|
ChatClient chatClient, |
||||
|
String prompt, |
||||
|
FluxSink<String> sink) { |
||||
|
|
||||
|
return Flux.create(innerSink -> { |
||||
|
try { |
||||
|
// 1. 发送开始信号 |
||||
|
innerSink.next(formatMessage("start", "开始分析...")); |
||||
|
|
||||
|
// 2. 收集AI的流式响应 |
||||
|
StringBuilder fullResponse = new StringBuilder(); |
||||
|
|
||||
|
chatClient.prompt() |
||||
|
.user(prompt) |
||||
|
.stream() |
||||
|
.content() |
||||
|
.subscribe( |
||||
|
chunk -> { |
||||
|
fullResponse.append(chunk); |
||||
|
// 实时发送字符(实现打字效果) |
||||
|
sendCharacterByCharacter(chunk, innerSink); |
||||
|
}, |
||||
|
error -> { |
||||
|
innerSink.next(formatMessage("error", |
||||
|
"分析失败: " + error.getMessage())); |
||||
|
innerSink.complete(); |
||||
|
}, |
||||
|
() -> { |
||||
|
// 所有内容发送完成后,发送完成信号 |
||||
|
try { |
||||
|
Thread.sleep(500); |
||||
|
innerSink.next(formatMessage("complete", "分析完成")); |
||||
|
innerSink.complete(); |
||||
|
} catch (InterruptedException e) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
innerSink.next(formatMessage("error", "流式处理错误: " + e.getMessage())); |
||||
|
innerSink.complete(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 逐字符发送(实现打字效果) |
||||
|
*/ |
||||
|
private static void sendCharacterByCharacter(String text, FluxSink<String> sink) { |
||||
|
if (text == null || text.isEmpty()) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// 按字符分割 |
||||
|
char[] chars = text.toCharArray(); |
||||
|
StringBuilder currentChunk = new StringBuilder(); |
||||
|
|
||||
|
for (int i = 0; i < chars.length; i++) { |
||||
|
currentChunk.append(chars[i]); |
||||
|
|
||||
|
// 遇到特殊字符或达到chunk大小,就发送一次 |
||||
|
if (shouldSendNow(chars[i], currentChunk.length(), i, chars)) { |
||||
|
sink.next(formatMessage("content", currentChunk.toString())); |
||||
|
currentChunk.setLength(0); |
||||
|
|
||||
|
// 根据字符类型添加不同的延迟 |
||||
|
int delay = calculateDelay(chars[i]); |
||||
|
if (delay > 0) { |
||||
|
Thread.sleep(delay); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 发送剩余字符 |
||||
|
if (currentChunk.length() > 0) { |
||||
|
sink.next(formatMessage("content", currentChunk.toString())); |
||||
|
} |
||||
|
|
||||
|
} catch (InterruptedException e) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断是否应该现在发送 |
||||
|
*/ |
||||
|
private static boolean shouldSendNow(char currentChar, int chunkSize, int index, char[] allChars) { |
||||
|
// 达到chunk大小 |
||||
|
if (chunkSize >= CHARS_PER_CHUNK) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 遇到换行符、标点符号等 |
||||
|
if (currentChar == '\n' || currentChar == '\r') { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 遇到中文字符(中文字符一般单独发送) |
||||
|
if (isChineseChar(currentChar)) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 遇到模块标题开始 |
||||
|
if (index > 0 && allChars[index-1] == '\n' && currentChar == '[') { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算延迟时间 |
||||
|
*/ |
||||
|
private static int calculateDelay(char c) { |
||||
|
// 换行符延迟稍长 |
||||
|
if (c == '\n' || c == '\r') { |
||||
|
return LINE_BREAK_DELAY_MS; |
||||
|
} |
||||
|
|
||||
|
// 标点符号延迟 |
||||
|
if (isPunctuation(c)) { |
||||
|
return 50; |
||||
|
} |
||||
|
|
||||
|
// 中文字符延迟 |
||||
|
if (isChineseChar(c)) { |
||||
|
return 30; |
||||
|
} |
||||
|
|
||||
|
// 英文字符延迟 |
||||
|
if (Character.isLetter(c)) { |
||||
|
return 20; |
||||
|
} |
||||
|
|
||||
|
// 数字和普通字符 |
||||
|
return 10; |
||||
|
} |
||||
|
|
||||
|
private static boolean isChineseChar(char c) { |
||||
|
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c); |
||||
|
return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS |
||||
|
|| ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS |
||||
|
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A; |
||||
|
} |
||||
|
|
||||
|
private static boolean isPunctuation(char c) { |
||||
|
return c == ',' || c == '.' || c == ';' || c == ':' || c == '!' || c == '?'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 格式化消息为JSON |
||||
|
*/ |
||||
|
public static String formatMessage(String type, String content) { |
||||
|
try { |
||||
|
Map<String, Object> message = new HashMap<>(); |
||||
|
message.put("type", type); |
||||
|
message.put("content", content); |
||||
|
message.put("timestamp", System.currentTimeMillis()); |
||||
|
|
||||
|
return objectMapper.writeValueAsString(message); |
||||
|
} catch (Exception e) { |
||||
|
return String.format("{\"type\":\"%s\",\"content\":\"%s\",\"timestamp\":%d}", |
||||
|
type, content.replace("\"", "\\\""), System.currentTimeMillis()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 优化分析提示词,确保AI返回正确格式 |
||||
|
*/ |
||||
|
public static String optimizePromptForStreaming(String originalPrompt) { |
||||
|
return originalPrompt + "\n\n" + |
||||
|
"【重要格式要求】\n" + |
||||
|
"1. 每个模块标题独立一行:[模块X: 标题]\n" + |
||||
|
"2. 标题后空一行再开始内容\n" + |
||||
|
"3. 模块之间用两个换行符分隔\n" + |
||||
|
"4. 保持简洁,每行不要过长\n" + |
||||
|
"5. 关键数据用**包裹\n" + |
||||
|
"6. 严格按照模板结构输出"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
package com.chenhai.chenhaiai.utils; |
||||
|
|
||||
|
import reactor.core.publisher.FluxSink; |
||||
|
|
||||
|
public class ProgressEmitter { |
||||
|
private FluxSink<String> sink; |
||||
|
|
||||
|
public void setSink(FluxSink<String> sink) { |
||||
|
this.sink = sink; |
||||
|
} |
||||
|
|
||||
|
public void emitProgress(String nodeName, String message) { |
||||
|
if (sink != null) { |
||||
|
String progressMsg = CharacterStreamProcessor.formatMessage("progress", |
||||
|
"[" + getFriendlyNodeName(nodeName) + "] " + message); |
||||
|
sink.next(progressMsg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String getFriendlyNodeName(String nodeName) { |
||||
|
// 简化节点名显示 |
||||
|
return nodeName.replace("JdbcNode", "").replace("Node", ""); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,277 @@ |
|||||
|
package com.chenhai.chenhaiai.utils; |
||||
|
|
||||
|
import org.springframework.core.io.ClassPathResource; |
||||
|
import org.springframework.core.io.Resource; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.springframework.util.FileCopyUtils; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import java.io.*; |
||||
|
import java.net.URL; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.Enumeration; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.jar.JarEntry; |
||||
|
import java.util.jar.JarFile; |
||||
|
|
||||
|
@Component |
||||
|
public class PromptLoader { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(PromptLoader.class); |
||||
|
private final Map<String, String> promptCache = new ConcurrentHashMap<>(); |
||||
|
private boolean debugMode = true; // 生产环境可设为false |
||||
|
|
||||
|
/** |
||||
|
* 从classpath加载提示词文件 - 保持原有API不变 |
||||
|
*/ |
||||
|
public String loadPrompt(String filePath) throws IOException { |
||||
|
// 先从缓存获取 |
||||
|
if (promptCache.containsKey(filePath)) { |
||||
|
return promptCache.get(filePath); |
||||
|
} |
||||
|
|
||||
|
if (debugMode) { |
||||
|
logger.info("开始加载提示词文件: {}", filePath); |
||||
|
} |
||||
|
|
||||
|
// 尝试多种加载方案 |
||||
|
String content = tryAllLoadingStrategies(filePath); |
||||
|
if (content != null) { |
||||
|
promptCache.put(filePath, content); |
||||
|
if (debugMode) { |
||||
|
logger.info("✅ 提示词加载成功: {}", filePath); |
||||
|
} |
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
throw new IOException("提示词文件不存在: " + filePath); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 尝试所有可能的加载策略 |
||||
|
*/ |
||||
|
private String tryAllLoadingStrategies(String filePath) { |
||||
|
// 策略1: 使用Spring的ClassPathResource(原有方式) |
||||
|
String content = loadWithClassPathResource(filePath); |
||||
|
if (content != null) return content; |
||||
|
|
||||
|
// 策略2: 直接使用ClassLoader(JAR包内访问) |
||||
|
content = loadWithClassLoader(filePath); |
||||
|
if (content != null) return content; |
||||
|
|
||||
|
// 策略3: 尝试相对路径 |
||||
|
content = loadWithClassLoader("prompts/" + filePath); |
||||
|
if (content != null) return content; |
||||
|
|
||||
|
// 策略4: 尝试绝对路径 |
||||
|
content = loadWithClassLoader("/prompts/" + filePath); |
||||
|
if (content != null) return content; |
||||
|
|
||||
|
// 策略5: 尝试BOOT-INF路径(Spring Boot打包路径) |
||||
|
content = loadWithClassLoader("BOOT-INF/classes/prompts/" + filePath); |
||||
|
if (content != null) return content; |
||||
|
|
||||
|
// 策略6: 如果以上都失败,输出调试信息 |
||||
|
if (debugMode) { |
||||
|
logDebugInfo(filePath); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 使用Spring的ClassPathResource(原有方式) |
||||
|
*/ |
||||
|
private String loadWithClassPathResource(String filePath) { |
||||
|
try { |
||||
|
Resource resource = new ClassPathResource(filePath); |
||||
|
if (resource.exists()) { |
||||
|
try (InputStreamReader reader = new InputStreamReader( |
||||
|
resource.getInputStream(), StandardCharsets.UTF_8)) { |
||||
|
return FileCopyUtils.copyToString(reader); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
if (debugMode) { |
||||
|
logger.debug("ClassPathResource加载失败: {} - {}", filePath, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 使用ClassLoader直接加载(JAR包内访问) |
||||
|
*/ |
||||
|
private String loadWithClassLoader(String path) { |
||||
|
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path)) { |
||||
|
if (inputStream != null) { |
||||
|
return readInputStreamContent(inputStream); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
if (debugMode) { |
||||
|
logger.debug("ClassLoader加载失败: {} - {}", path, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 从输入流读取内容 |
||||
|
*/ |
||||
|
private String readInputStreamContent(InputStream inputStream) throws IOException { |
||||
|
try (BufferedReader reader = new BufferedReader( |
||||
|
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { |
||||
|
StringBuilder content = new StringBuilder(); |
||||
|
String line; |
||||
|
while ((line = reader.readLine()) != null) { |
||||
|
content.append(line).append("\n"); |
||||
|
} |
||||
|
return content.toString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 输出调试信息 |
||||
|
*/ |
||||
|
private void logDebugInfo(String filePath) { |
||||
|
try { |
||||
|
logger.warn("=== 提示词文件查找失败,开始调试 ==="); |
||||
|
logger.warn("查找文件: {}", filePath); |
||||
|
|
||||
|
// 列出所有可用的prompts文件 |
||||
|
Enumeration<URL> resources = getClass().getClassLoader().getResources("prompts"); |
||||
|
boolean foundResources = false; |
||||
|
|
||||
|
while (resources.hasMoreElements()) { |
||||
|
foundResources = true; |
||||
|
URL url = resources.nextElement(); |
||||
|
logger.warn("资源位置: {}", url); |
||||
|
|
||||
|
if ("jar".equals(url.getProtocol())) { |
||||
|
listJarContents(url); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!foundResources) { |
||||
|
logger.warn("未找到任何prompts目录资源"); |
||||
|
} |
||||
|
|
||||
|
// 测试常见文件路径 |
||||
|
testCommonFilePaths(); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
logger.error("调试信息输出失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 列出JAR包内容 |
||||
|
*/ |
||||
|
private void listJarContents(URL jarUrl) { |
||||
|
try { |
||||
|
String jarPath = jarUrl.getPath(); |
||||
|
if (jarPath.startsWith("file:")) { |
||||
|
jarPath = jarPath.substring(5); |
||||
|
} |
||||
|
if (jarPath.contains("!")) { |
||||
|
jarPath = jarPath.substring(0, jarPath.indexOf("!")); |
||||
|
} |
||||
|
|
||||
|
logger.warn("扫描JAR文件: {}", jarPath); |
||||
|
|
||||
|
try (JarFile jarFile = new JarFile(jarPath)) { |
||||
|
Enumeration<JarEntry> entries = jarFile.entries(); |
||||
|
int promptFileCount = 0; |
||||
|
|
||||
|
while (entries.hasMoreElements()) { |
||||
|
JarEntry entry = entries.nextElement(); |
||||
|
String name = entry.getName(); |
||||
|
if (name.contains("prompts") && name.endsWith(".txt")) { |
||||
|
promptFileCount++; |
||||
|
if (promptFileCount <= 10) { // 只显示前10个文件 |
||||
|
logger.warn("JAR中的提示词文件: {}", name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
logger.warn("找到 {} 个提示词文件", promptFileCount); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
logger.error("扫描JAR文件失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 测试常见文件路径 |
||||
|
*/ |
||||
|
private void testCommonFilePaths() { |
||||
|
String[] testFiles = { |
||||
|
"week_plan_analysis4.txt", |
||||
|
"management-perspective.txt", |
||||
|
"process-perspective.txt", |
||||
|
"culture-perspective.txt", |
||||
|
"comprehensive-perspective.txt" |
||||
|
}; |
||||
|
|
||||
|
String[] testPaths = { |
||||
|
"prompts/", |
||||
|
"/prompts/", |
||||
|
"BOOT-INF/classes/prompts/" |
||||
|
}; |
||||
|
|
||||
|
for (String file : testFiles) { |
||||
|
for (String path : testPaths) { |
||||
|
String fullPath = path + file; |
||||
|
InputStream stream = getClass().getClassLoader().getResourceAsStream(fullPath); |
||||
|
if (stream != null) { |
||||
|
logger.warn("✅ 测试成功: {}", fullPath); |
||||
|
try { stream.close(); } catch (IOException e) {} |
||||
|
} else { |
||||
|
logger.warn("❌ 测试失败: {}", fullPath); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 加载提示词并替换占位符 - 保持原有API不变 |
||||
|
*/ |
||||
|
public String loadPrompt(String filePath, Map<String, String> placeholders) throws IOException { |
||||
|
String template = loadPrompt(filePath); |
||||
|
|
||||
|
// 替换占位符 |
||||
|
for (Map.Entry<String, String> entry : placeholders.entrySet()) { |
||||
|
template = template.replace("{" + entry.getKey() + "}", entry.getValue()); |
||||
|
} |
||||
|
|
||||
|
return template; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 预加载常用提示词 - 保持原有逻辑 |
||||
|
*/ |
||||
|
@PostConstruct |
||||
|
public void preloadPrompts() { |
||||
|
try { |
||||
|
// 预加载周计划分析提示词 |
||||
|
loadPrompt("prompts/week_plan_analysis4.txt"); |
||||
|
if (debugMode) { |
||||
|
logger.info("提示词预加载完成"); |
||||
|
} |
||||
|
} catch (IOException e) { |
||||
|
logger.error("提示词预加载失败: {}", e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清空缓存(用于调试) |
||||
|
*/ |
||||
|
public void clearCache() { |
||||
|
promptCache.clear(); |
||||
|
if (debugMode) { |
||||
|
logger.info("提示词缓存已清空"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
// TextFormatUtils.java |
||||
|
package com.chenhai.chenhaiai.utils; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 文本格式处理工具类 |
||||
|
*/ |
||||
|
public class TextFormatUtils { |
||||
|
|
||||
|
private static final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
/** |
||||
|
* 格式化消息为JSON字符串 |
||||
|
*/ |
||||
|
public static String formatMessage(String type, String content) { |
||||
|
try { |
||||
|
Map<String, Object> message = new HashMap<>(); |
||||
|
message.put("type", type); |
||||
|
message.put("content", content); |
||||
|
message.put("timestamp", System.currentTimeMillis()); |
||||
|
|
||||
|
return objectMapper.writeValueAsString(message); |
||||
|
} catch (Exception e) { |
||||
|
// 如果JSON序列化失败,使用简单格式 |
||||
|
return String.format("{\"type\":\"%s\",\"content\":\"%s\",\"timestamp\":%d}", |
||||
|
type, |
||||
|
escapeJson(content), |
||||
|
System.currentTimeMillis() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 转义JSON特殊字符 |
||||
|
*/ |
||||
|
private static String escapeJson(String content) { |
||||
|
if (content == null) { |
||||
|
return ""; |
||||
|
} |
||||
|
return content.replace("\\", "\\\\") |
||||
|
.replace("\"", "\\\"") |
||||
|
.replace("\n", "\\n") |
||||
|
.replace("\r", "\\r") |
||||
|
.replace("\t", "\\t"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 确保分析内容有正确的换行格式 |
||||
|
*/ |
||||
|
public static String ensureAnalysisFormat(String content) { |
||||
|
if (content == null || content.isEmpty()) { |
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
// 1. 确保 [模块X: 标题] 前面有换行(如果不是第一个模块) |
||||
|
String formatted = content.replaceAll("(?<!^)\\[模块", "\n\n[模块"); |
||||
|
|
||||
|
// 2. 确保每个模块标题后有换行 |
||||
|
formatted = formatted.replaceAll("\\[模块(\\d+): ([^\\]]+)\\]", "[模块$1: $2]\n"); |
||||
|
|
||||
|
// 3. 去掉开头可能产生的多余空行 |
||||
|
formatted = formatted.trim(); |
||||
|
|
||||
|
return formatted; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 分割分析结果为模块数组 |
||||
|
*/ |
||||
|
public static String[] splitAnalysisModules(String analysisContent) { |
||||
|
if (analysisContent == null || analysisContent.isEmpty()) { |
||||
|
return new String[0]; |
||||
|
} |
||||
|
|
||||
|
// 按 "---" 分割模块,但保留分割符 |
||||
|
return analysisContent.split("(?=---)"); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue