commit 9d100f9465c269fc6eedf82558de8d47f9a81850 Author: will Date: Wed Aug 5 16:38:56 2026 +0800 feat(agent): 添加AI智能代理功能模块 - 集成Spring AI框架支持大模型对话 - 实现聊天历史记录管理功能 - 添加代码生成功能支持 - 集成MCP协议工具调用 - 实现流式响应和会话管理 - 添加系统提示词和上下文管理 - 支持多租户会话隔离 - 配置自动化装配和依赖注入 - 实现错误处理和异常捕获 - 添加用户权限验证和审计追踪 - 支持对话历史持久化存储 - 实现智能时间上下文识别 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.gradle-codex/caches/8.13/dependencies-accessors/gc.properties b/.gradle-codex/caches/8.13/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/8.13/file-changes/last-build.bin b/.gradle-codex/caches/8.13/file-changes/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.gradle-codex/caches/8.13/file-changes/last-build.bin differ diff --git a/.gradle-codex/caches/8.13/fileContent/fileContent.lock b/.gradle-codex/caches/8.13/fileContent/fileContent.lock new file mode 100644 index 0000000..8fb69eb Binary files /dev/null and b/.gradle-codex/caches/8.13/fileContent/fileContent.lock differ diff --git a/.gradle-codex/caches/8.13/fileHashes/fileHashes.bin b/.gradle-codex/caches/8.13/fileHashes/fileHashes.bin new file mode 100644 index 0000000..6907523 Binary files /dev/null and b/.gradle-codex/caches/8.13/fileHashes/fileHashes.bin differ diff --git a/.gradle-codex/caches/8.13/fileHashes/fileHashes.lock b/.gradle-codex/caches/8.13/fileHashes/fileHashes.lock new file mode 100644 index 0000000..15c7a0d Binary files /dev/null and b/.gradle-codex/caches/8.13/fileHashes/fileHashes.lock differ diff --git a/.gradle-codex/caches/8.13/gc.properties b/.gradle-codex/caches/8.13/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/classes/settings/_BuildScript_.class b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/classes/settings/_BuildScript_.class new file mode 100644 index 0000000..51891e0 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/classes/settings/_BuildScript_.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/instrumented/settings/_BuildScript_.class b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/instrumented/settings/_BuildScript_.class new file mode 100644 index 0000000..c447965 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/instrumented/settings/_BuildScript_.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata.bin b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata.bin new file mode 100644 index 0000000..88d8fe7 --- /dev/null +++ b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata.bin @@ -0,0 +1,2 @@ +›5frzku4ad5girive24os32j44yu‹^ΒVIG�/k9r•(“instrumentedOutputڢHŽ€Ò9&�ôŒ(¥ŒmetadataDirӫŽ�ž#4Oõe +ð؉œpropertyUpgradeReportOutput²´Æyàޜ3Ÿ&þÍË;ŒÕ \ No newline at end of file diff --git a/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata/metadata.bin b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata/metadata.bin new file mode 100644 index 0000000..bdc955b Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata/metadata.bin differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_$_run_closure1.class b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_$_run_closure1.class new file mode 100644 index 0000000..9f8136e Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_$_run_closure1.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_.class b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_.class new file mode 100644 index 0000000..4360ab7 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_$_run_closure1.class b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_$_run_closure1.class new file mode 100644 index 0000000..2658aaf Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_$_run_closure1.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_.class b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_.class new file mode 100644 index 0000000..d97125b Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata.bin b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata.bin new file mode 100644 index 0000000..23270ce Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata.bin differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata/metadata.bin b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata/metadata.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata/metadata.bin differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure2.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure2.class new file mode 100644 index 0000000..ab520b9 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure2.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure3.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure3.class new file mode 100644 index 0000000..53bbaf1 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure3.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1.class new file mode 100644 index 0000000..77e443d Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_.class new file mode 100644 index 0000000..da258bd Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure2.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure2.class new file mode 100644 index 0000000..31b8b2e Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure2.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure3.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure3.class new file mode 100644 index 0000000..577d18e Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure3.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1.class new file mode 100644 index 0000000..7e0d5b9 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_.class b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_.class new file mode 100644 index 0000000..846082d Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_.class differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata.bin b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata.bin new file mode 100644 index 0000000..2fa0564 --- /dev/null +++ b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata.bin @@ -0,0 +1 @@ +›5frzku4ad5girive24os32j44yk%wŸáô@*a¿Ñ5þ]¯¯Â“instrumentedOutputW©ˇW§ÜY^ÍçÐóOŒmetadataDir�)CÇäe9=É*òs@‘ÈœpropertyUpgradeReportOutput²´Æyàޜ3Ÿ&þÍË;ŒÕ \ No newline at end of file diff --git a/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata/metadata.bin b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata/metadata.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata/metadata.bin differ diff --git a/.gradle-codex/caches/8.13/groovy-dsl/gc.properties b/.gradle-codex/caches/8.13/groovy-dsl/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/8.13/md-rule/md-rule.lock b/.gradle-codex/caches/8.13/md-rule/md-rule.lock new file mode 100644 index 0000000..70c35e6 Binary files /dev/null and b/.gradle-codex/caches/8.13/md-rule/md-rule.lock differ diff --git a/.gradle-codex/caches/8.13/md-supplier/md-supplier.lock b/.gradle-codex/caches/8.13/md-supplier/md-supplier.lock new file mode 100644 index 0000000..1820db5 Binary files /dev/null and b/.gradle-codex/caches/8.13/md-supplier/md-supplier.lock differ diff --git a/.gradle-codex/caches/8.13/transforms/gc.properties b/.gradle-codex/caches/8.13/transforms/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/CACHEDIR.TAG b/.gradle-codex/caches/CACHEDIR.TAG new file mode 100644 index 0000000..c8907d7 --- /dev/null +++ b/.gradle-codex/caches/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by Gradle. +# For information about cache directory tags, see: +# https://bford.info/cachedir/ \ No newline at end of file diff --git a/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj.lock.lock b/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj.lock.lock new file mode 100644 index 0000000..35a0387 Binary files /dev/null and b/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj.lock.lock differ diff --git a/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj.receipt b/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj.receipt new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9$_run_closure1.class b/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9$_run_closure1.class new file mode 100644 index 0000000..2f343a1 Binary files /dev/null and b/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9$_run_closure1.class differ diff --git a/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9.class b/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9.class new file mode 100644 index 0000000..d5fe60c Binary files /dev/null and b/.gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9.class differ diff --git a/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings.lock.lock b/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings.lock.lock new file mode 100644 index 0000000..35a0387 Binary files /dev/null and b/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings.lock.lock differ diff --git a/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings.receipt b/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings.receipt new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class b/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class new file mode 100644 index 0000000..27848da Binary files /dev/null and b/.gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class differ diff --git a/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings.lock.lock b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings.lock.lock new file mode 100644 index 0000000..35a0387 Binary files /dev/null and b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings.lock.lock differ diff --git a/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings.receipt b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings.receipt new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure2.class b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure2.class new file mode 100644 index 0000000..1d94e09 Binary files /dev/null and b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure2.class differ diff --git a/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure3.class b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure3.class new file mode 100644 index 0000000..2ccddf8 Binary files /dev/null and b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure3.class differ diff --git a/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1.class b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1.class new file mode 100644 index 0000000..1f450fb Binary files /dev/null and b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1.class differ diff --git a/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class new file mode 100644 index 0000000..fd52c85 Binary files /dev/null and b/.gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class differ diff --git a/.gradle-codex/caches/jars-9/gc.properties b/.gradle-codex/caches/jars-9/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/jars-9/jars-9.lock b/.gradle-codex/caches/jars-9/jars-9.lock new file mode 100644 index 0000000..bd875ff Binary files /dev/null and b/.gradle-codex/caches/jars-9/jars-9.lock differ diff --git a/.gradle-codex/caches/journal-1/file-access.bin b/.gradle-codex/caches/journal-1/file-access.bin new file mode 100644 index 0000000..47376e1 Binary files /dev/null and b/.gradle-codex/caches/journal-1/file-access.bin differ diff --git a/.gradle-codex/caches/journal-1/file-access.properties b/.gradle-codex/caches/journal-1/file-access.properties new file mode 100644 index 0000000..7ae18f7 --- /dev/null +++ b/.gradle-codex/caches/journal-1/file-access.properties @@ -0,0 +1,2 @@ +#Wed Aug 05 00:21:13 CST 2026 +inceptionTimestamp=1785860473531 diff --git a/.gradle-codex/caches/journal-1/journal-1.lock b/.gradle-codex/caches/journal-1/journal-1.lock new file mode 100644 index 0000000..69d7683 Binary files /dev/null and b/.gradle-codex/caches/journal-1/journal-1.lock differ diff --git a/.gradle-codex/caches/modules-2/gc.properties b/.gradle-codex/caches/modules-2/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle-codex/caches/modules-2/metadata-2.107/module-metadata.bin b/.gradle-codex/caches/modules-2/metadata-2.107/module-metadata.bin new file mode 100644 index 0000000..7c2e6cd Binary files /dev/null and b/.gradle-codex/caches/modules-2/metadata-2.107/module-metadata.bin differ diff --git a/.gradle-codex/caches/modules-2/metadata-2.107/resource-at-url.bin b/.gradle-codex/caches/modules-2/metadata-2.107/resource-at-url.bin new file mode 100644 index 0000000..7c2e6cd Binary files /dev/null and b/.gradle-codex/caches/modules-2/metadata-2.107/resource-at-url.bin differ diff --git a/.gradle-codex/caches/modules-2/modules-2.lock b/.gradle-codex/caches/modules-2/modules-2.lock new file mode 100644 index 0000000..d634761 Binary files /dev/null and b/.gradle-codex/caches/modules-2/modules-2.lock differ diff --git a/.gradle-codex/daemon/8.13/daemon-24024.out.log b/.gradle-codex/daemon/8.13/daemon-24024.out.log new file mode 100644 index 0000000..a300d96 --- /dev/null +++ b/.gradle-codex/daemon/8.13/daemon-24024.out.log @@ -0,0 +1,446 @@ +2026-08-05T00:21:04.804+0800 [DEBUG] [org.gradle.launcher.daemon.bootstrap.DaemonMain] Assuming the daemon was started with following jvm opts: [-XX:MaxMetaspaceSize=384m, -XX:+HeapDumpOnOutOfMemoryError, -Xms256m, -Xmx512m, -Dfile.encoding=UTF-8, -Duser.country=CN, -Duser.language=zh, -Duser.variant] +2026-08-05T00:21:04.958+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface iNode VPN Virtual NIC-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.960+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.960+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface iNode VPN Virtual NIC-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.961+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.962+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface iNode VPN Virtual NIC-WFP 802.3 MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.963+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.963+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IP)-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.963+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.963+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IP)-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.964+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.964+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IPv6)-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.964+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.964+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IPv6)-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.964+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.965+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (Network Monitor)-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.965+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.965+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (Network Monitor)-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.965+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.966+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Kernel Debug Network Adapter +2026-08-05T00:21:04.966+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.966+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface ASIX AX88772C USB2.0 to Fast Ethernet Adapter +2026-08-05T00:21:04.966+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.966+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Bluetooth Device (Personal Area Network) +2026-08-05T00:21:04.967+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.967+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:8450:47d:cf7a:cdb5%ethernet_32770 +2026-08-05T00:21:04.967+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IP) +2026-08-05T00:21:04.967+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.967+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IPv6) +2026-08-05T00:21:04.967+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.968+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (Network Monitor) +2026-08-05T00:21:04.968+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.968+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Cisco AnyConnect Virtual Miniport Adapter for Windows x64 +2026-08-05T00:21:04.968+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.968+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface iNode VPN Virtual NIC +2026-08-05T00:21:04.969+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.970+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:53eb:3262:161:3f0f%ethernet_32775 +2026-08-05T00:21:04.970+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (PPPOE) +2026-08-05T00:21:04.970+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.970+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Software Loopback Interface 1 +2026-08-05T00:21:04.970+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? true +2026-08-05T00:21:04.970+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding loopback address /0:0:0:0:0:0:0:1 +2026-08-05T00:21:04.971+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding loopback address /127.0.0.1 +2026-08-05T00:21:04.971+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.972+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.972+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.972+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.972+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9-WFP 802.3 MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.972+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.973+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Sangfor aTrust VNIC-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.973+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.973+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Sangfor aTrust VNIC-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.973+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.973+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Sangfor aTrust VNIC-WFP 802.3 MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.974+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.974+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9 +2026-08-05T00:21:04.974+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.974+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:c9bb:cc68:e870:45ba%iftype53_32768 +2026-08-05T00:21:04.974+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /192.168.255.214 +2026-08-05T00:21:04.974+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface OpenVPN Data Channel Offload +2026-08-05T00:21:04.975+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.975+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:54a8:839c:5e41:a357%iftype53_32769 +2026-08-05T00:21:04.975+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Sangfor aTrust VNIC +2026-08-05T00:21:04.976+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.976+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:cb45:bb4c:df23:c8af%iftype53_32770 +2026-08-05T00:21:04.976+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Intel(R) Wi-Fi 6 AX203-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.976+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.976+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Intel(R) Wi-Fi 6 AX203-Virtual WiFi Filter Driver-0000 +2026-08-05T00:21:04.977+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.977+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Intel(R) Wi-Fi 6 AX203-Native WiFi Filter Driver-0000 +2026-08-05T00:21:04.977+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.977+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Intel(R) Wi-Fi 6 AX203-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.978+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.978+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Intel(R) Wi-Fi 6 AX203-WFP 802.3 MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.978+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.978+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.978+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.978+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.979+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.979+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter-Native WiFi Filter Driver-0000 +2026-08-05T00:21:04.979+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.979+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter-WFP 802.3 MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.979+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.979+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #2-WFP Native MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.979+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.980+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #2-Native WiFi Filter Driver-0000 +2026-08-05T00:21:04.980+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.980+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #2-QoS Packet Scheduler-0000 +2026-08-05T00:21:04.980+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.980+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #2-WFP 802.3 MAC Layer LightWeight Filter-0000 +2026-08-05T00:21:04.980+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.980+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Intel(R) Wi-Fi 6 AX203 +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:e7b4:8c35:b44f:aaf2%wireless_32768 +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /240e:3a1:569e:f7d1:d17a:f850:422:690c +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /240e:3a1:569e:f7d1:1451:2096:4290:2 +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /240e:3a1:569e:f7d1:d0e:75bf:d9f1:ee06 +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /192.168.3.18 +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter +2026-08-05T00:21:04.981+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.982+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:cf5:3e11:24da:b8ea%wireless_32769 +2026-08-05T00:21:04.982+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #2 +2026-08-05T00:21:04.982+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.982+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:eaf6:db93:6b4f:9b79%wireless_32770 +2026-08-05T00:21:04.982+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Teredo Tunneling Adapter +2026-08-05T00:21:04.982+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.982+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft IP-HTTPS Platform Adapter +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft 6to4 Adapter +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (SSTP) +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IKEv2) +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (L2TP) +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:04.983+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (PPTP) +2026-08-05T00:21:04.984+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false +2026-08-05T00:21:05.000+0800 [DEBUG] [org.gradle.internal.nativeintegration.services.NativeServices] Native-platform posix files integration is not available. Continuing with fallback. +2026-08-05T00:21:05.028+0800 [INFO] [org.gradle.launcher.daemon.server.Daemon] start() called on daemon - DefaultDaemonContext[uid=97be2d6a-15b4-4340-995d-c7d8c8274827,javaHome=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=D:\WORK\SELF\willSystem\willSystem\.gradle-codex\daemon,pid=24024,idleTimeout=120000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant] +2026-08-05T00:21:13.064+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.TcpIncomingConnector] Listening on [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]]. +2026-08-05T00:21:13.079+0800 [DEBUG] [org.gradle.launcher.daemon.server.Daemon] Daemon starting at: Wed Aug 05 00:21:13 CST 2026, with address: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:13.079+0800 [INFO] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Advertising the daemon address to the clients: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:13.079+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Advertised daemon context: DefaultDaemonContext[uid=97be2d6a-15b4-4340-995d-c7d8c8274827,javaHome=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=D:\WORK\SELF\willSystem\willSystem\.gradle-codex\daemon,pid=24024,idleTimeout=120000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant] +2026-08-05T00:21:13.081+0800 [DEBUG] [org.gradle.launcher.daemon.registry.PersistentDaemonRegistry] Storing daemon address: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]], context: DefaultDaemonContext[uid=97be2d6a-15b4-4340-995d-c7d8c8274827,javaHome=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=D:\WORK\SELF\willSystem\willSystem\.gradle-codex\daemon,pid=24024,idleTimeout=120000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant] +2026-08-05T00:21:13.096+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on daemon addresses registry. +2026-08-05T00:21:13.101+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry. +2026-08-05T00:21:13.102+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry. +2026-08-05T00:21:13.105+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on daemon addresses registry. +2026-08-05T00:21:13.106+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry. +2026-08-05T00:21:13.109+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry. +2026-08-05T00:21:13.110+0800 [LIFECYCLE] [org.gradle.launcher.daemon.server.Daemon] Daemon server started. +2026-08-05T00:21:13.111+0800 [DEBUG] [org.gradle.launcher.daemon.bootstrap.DaemonStartupCommunication] Completed writing the daemon greeting. Closing streams... +2026-08-05T00:21:13.120+0800 [DEBUG] [org.gradle.launcher.daemon.server.Daemon] stopOnExpiration() called on daemon +2026-08-05T00:21:13.121+0800 [DEBUG] [org.gradle.launcher.daemon.server.Daemon] awaitExpiration() called on daemon +2026-08-05T00:21:13.122+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] daemon is running. Sleeping until state changes. +2026-08-05T00:21:13.129+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.TcpIncomingConnector] Accepted connection from /127.0.0.1:50342 to /127.0.0.1:50340. +2026-08-05T00:21:13.207+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 78: received class org.gradle.launcher.daemon.protocol.Build +2026-08-05T00:21:13.208+0800 [DEBUG] [org.gradle.launcher.daemon.server.DefaultDaemonConnection] thread 78: Received non-IO message from client: Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem} +2026-08-05T00:21:13.208+0800 [INFO] [org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler] Received command: Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem}. +2026-08-05T00:21:13.208+0800 [DEBUG] [org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler] Starting executing command: Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem} with connection: socket connection from /127.0.0.1:50340 to /127.0.0.1:50342. +2026-08-05T00:21:13.212+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Command execution: started DaemonCommandExecution[command = Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem}, connection = DefaultDaemonConnection: socket connection from /127.0.0.1:50340 to /127.0.0.1:50342] after 0.002533333333333333 minutes of idle +2026-08-05T00:21:13.212+0800 [INFO] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Marking the daemon as busy, address: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:13.212+0800 [DEBUG] [org.gradle.launcher.daemon.registry.PersistentDaemonRegistry] Marking busy by address: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:13.214+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on daemon addresses registry. +2026-08-05T00:21:13.214+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry. +2026-08-05T00:21:13.216+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry. +2026-08-05T00:21:13.217+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] resetting idle timer +2026-08-05T00:21:13.217+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] daemon is running. Sleeping until state changes. +2026-08-05T00:21:13.218+0800 [INFO] [org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy] Daemon is about to start building Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem}. Dispatching build started information... +2026-08-05T00:21:13.218+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 80: dispatching org.gradle.launcher.daemon.protocol.BuildStarted@38452d58 +2026-08-05T00:21:13.220+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment] Configuring env variables: [PATH, USERDOMAIN_ROAMINGPROFILE, PROCESSOR_LEVEL, GIT_PAGER, ALLUSERSPROFILE, CODEX_SHELL, PROCESSOR_ARCHITECTURE, GATEWAY_VM_OPTIONS, PSModulePath, SystemDrive, BROWSER_USE_AVAILABLE_BACKENDS, NODE_REPL_TRUSTED_BROWSER_CLIENT_SHA256S, DIRNAME, USERNAME, DEVECOSTUDIO_VM_OPTIONS, STUDIO_VM_OPTIONS, ProgramFiles(x86), CODEX_INTERNAL_ORIGINATOR_OVERRIDE, APP_HOME, CODEX_SANDBOX_NETWORK_DISABLED, DEFAULT_JVM_OPTS, PATHEXT, SHELL, DriverData, OneDriveConsumer, PAGER, NODE_REPL_TRUSTED_CODE_PATHS, CODEX_PERMISSION_PROFILE, LOG_FORMAT, ProgramData, ProgramW6432, HOMEPATH, PROCESSOR_IDENTIFIER, ProgramFiles, PUBLIC, ZSH_TMUX_AUTOSTARTED, LESS, windir, ZES_ENABLE_SYSMAN, LOCALAPPDATA, IntelliJ IDEA, USERDOMAIN, LOGONSERVER, JAVA_HOME, PROMPT, RUST_LOG, JETBRAINSCLIENT_VM_OPTIONS, GRADLE_HOME, OneDrive, APPDATA, GRADLE_USER_HOME, JAVA_EXE, CODEX_THREAD_ID, JETBRAINS_CLIENT_VM_OPTIONS, CommonProgramFiles, Path, DISABLE_AUTO_UPDATE, OS, COMPUTERNAME, PROCESSOR_REVISION, CLASSPATH, CommonProgramW6432, ComSpec, APP_BASE_NAME, ZSH_TMUX_AUTOSTART, =D:, SystemRoot, TEMP, WEBIDE_VM_OPTIONS, HOMEDRIVE, USERPROFILE, TMP, ZHIPU_API_KEY, CommonProgramFiles(x86), NUMBER_OF_PROCESSORS] +2026-08-05T00:21:13.225+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.LogToClient] About to start relaying all logs to the client via the connection. +2026-08-05T00:21:13.226+0800 [INFO] [org.gradle.launcher.daemon.server.exec.LogToClient] The client will now receive all logging from the daemon (pid: 24024). The daemon log file: D:\WORK\SELF\willSystem\willSystem\.gradle-codex\daemon\8.13\daemon-24024.out.log +2026-08-05T00:21:13.227+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon] Requesting daemon stop after processing Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem} +2026-08-05T00:21:13.227+0800 [LIFECYCLE] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Daemon will be stopped at the end of the build +2026-08-05T00:21:13.227+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Stop as soon as idle requested. The daemon is busy +2026-08-05T00:21:13.228+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] daemon stop has been requested. Sleeping until state changes. +2026-08-05T00:21:13.228+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] The daemon has started executing the build. +2026-08-05T00:21:13.228+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] Executing build with daemon context: DefaultDaemonContext[uid=97be2d6a-15b4-4340-995d-c7d8c8274827,javaHome=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=D:\WORK\SELF\willSystem\willSystem\.gradle-codex\daemon,pid=24024,idleTimeout=120000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant] + +FAILURE: Build failed with an exception. + +* Where: +Build file 'D:\WORK\SELF\willSystem\willSystem\build.gradle' line: 3 + +* What went wrong: +Plugin [id: 'io.spring.dependency-management', version: '1.1.7', apply: false] was not found in any of the following sources: + +- Gradle Core Plugins (plugin is not in 'org.gradle' namespace) +- Included Builds (No included builds contain this plugin) +- Plugin Repositories (could not resolve plugin artifact 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.1.7') + Searched in the following repositories: + MavenRepo + Gradle Central Plugin Repository + +* Try: +> Run with --info or --debug option to get more log output. +> Run with --scan to get full insights. +> Get more help at https://help.gradle.org. + +* Exception is: +org.gradle.api.plugins.UnknownPluginException: Plugin [id: 'io.spring.dependency-management', version: '1.1.7', apply: false] was not found in any of the following sources: + +- Gradle Core Plugins (plugin is not in 'org.gradle' namespace) +- Included Builds (No included builds contain this plugin) +- Plugin Repositories (could not resolve plugin artifact 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.1.7') + Searched in the following repositories: + MavenRepo + Gradle Central Plugin Repository + at org.gradle.plugin.use.resolve.internal.PluginResolutionResult.getFound(PluginResolutionResult.java:112) + at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.resolvePluginRequest(DefaultPluginRequestApplicator.java:197) + at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.applyPlugins(DefaultPluginRequestApplicator.java:101) + at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:123) + at org.gradle.configuration.BuildOperationScriptPlugin$1.run(BuildOperationScriptPlugin.java:68) + at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:30) + at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:27) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:48) + at org.gradle.configuration.BuildOperationScriptPlugin.lambda$apply$0(BuildOperationScriptPlugin.java:65) + at org.gradle.internal.code.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:44) + at org.gradle.configuration.BuildOperationScriptPlugin.apply(BuildOperationScriptPlugin.java:65) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:435) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:453) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:434) + at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:46) + at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:27) + at org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:35) + at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.lambda$run$0(LifecycleProjectEvaluator.java:109) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:435) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$fromMutableState$2(DefaultProjectStateRegistry.java:458) + at org.gradle.internal.work.DefaultWorkerLeaseService.withReplacedLocks(DefaultWorkerLeaseService.java:359) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:458) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:434) + at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.run(LifecycleProjectEvaluator.java:100) + at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:30) + at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:27) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:48) + at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:72) + at org.gradle.api.internal.project.DefaultProject.evaluateUnchecked(DefaultProject.java:828) + at org.gradle.api.internal.project.ProjectLifecycleController.lambda$ensureSelfConfigured$2(ProjectLifecycleController.java:89) + at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255) + at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266) + at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254) + at org.gradle.internal.model.StateTransitionController.lambda$maybeTransitionIfNotCurrentlyTransitioning$10(StateTransitionController.java:199) + at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:36) + at org.gradle.internal.model.StateTransitionController.maybeTransitionIfNotCurrentlyTransitioning(StateTransitionController.java:195) + at org.gradle.api.internal.project.ProjectLifecycleController.ensureSelfConfigured(ProjectLifecycleController.java:89) + at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.ensureConfigured(DefaultProjectStateRegistry.java:400) + at org.gradle.execution.TaskPathProjectEvaluator.configure(TaskPathProjectEvaluator.java:70) + at org.gradle.execution.TaskPathProjectEvaluator.configureHierarchy(TaskPathProjectEvaluator.java:84) + at org.gradle.configuration.DefaultProjectsPreparer.prepareProjects(DefaultProjectsPreparer.java:50) + at org.gradle.configuration.BuildTreePreparingProjectsPreparer.prepareProjects(BuildTreePreparingProjectsPreparer.java:65) + at org.gradle.configuration.BuildOperationFiringProjectsPreparer$ConfigureBuild.run(BuildOperationFiringProjectsPreparer.java:52) + at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:30) + at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:27) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:48) + at org.gradle.configuration.BuildOperationFiringProjectsPreparer.prepareProjects(BuildOperationFiringProjectsPreparer.java:40) + at org.gradle.initialization.VintageBuildModelController.lambda$prepareProjects$2(VintageBuildModelController.java:84) + at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255) + at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266) + at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254) + at org.gradle.internal.model.StateTransitionController.lambda$transitionIfNotPreviously$11(StateTransitionController.java:213) + at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:36) + at org.gradle.internal.model.StateTransitionController.transitionIfNotPreviously(StateTransitionController.java:209) + at org.gradle.initialization.VintageBuildModelController.prepareProjects(VintageBuildModelController.java:84) + at org.gradle.initialization.VintageBuildModelController.prepareToScheduleTasks(VintageBuildModelController.java:71) + at org.gradle.internal.build.DefaultBuildLifecycleController.lambda$prepareToScheduleTasks$6(DefaultBuildLifecycleController.java:175) + at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255) + at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266) + at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254) + at org.gradle.internal.model.StateTransitionController.lambda$maybeTransition$9(StateTransitionController.java:190) + at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:36) + at org.gradle.internal.model.StateTransitionController.maybeTransition(StateTransitionController.java:186) + at org.gradle.internal.build.DefaultBuildLifecycleController.prepareToScheduleTasks(DefaultBuildLifecycleController.java:173) + at org.gradle.internal.buildtree.DefaultBuildTreeWorkPreparer.scheduleRequestedTasks(DefaultBuildTreeWorkPreparer.java:36) + at org.gradle.internal.cc.impl.VintageBuildTreeWorkController$scheduleAndRunRequestedTasks$1.apply(VintageBuildTreeWorkController.kt:36) + at org.gradle.internal.cc.impl.VintageBuildTreeWorkController$scheduleAndRunRequestedTasks$1.apply(VintageBuildTreeWorkController.kt:35) + at org.gradle.composite.internal.DefaultIncludedBuildTaskGraph.withNewWorkGraph(DefaultIncludedBuildTaskGraph.java:112) + at org.gradle.internal.cc.impl.VintageBuildTreeWorkController.scheduleAndRunRequestedTasks(VintageBuildTreeWorkController.kt:35) + at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$scheduleAndRunTasks$1(DefaultBuildTreeLifecycleController.java:77) + at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$runBuild$4(DefaultBuildTreeLifecycleController.java:120) + at org.gradle.internal.model.StateTransitionController.lambda$transition$6(StateTransitionController.java:169) + at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266) + at org.gradle.internal.model.StateTransitionController.lambda$transition$7(StateTransitionController.java:169) + at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:46) + at org.gradle.internal.model.StateTransitionController.transition(StateTransitionController.java:169) + at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.runBuild(DefaultBuildTreeLifecycleController.java:117) + at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.scheduleAndRunTasks(DefaultBuildTreeLifecycleController.java:77) + at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.scheduleAndRunTasks(DefaultBuildTreeLifecycleController.java:72) + at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:31) + at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35) + at org.gradle.internal.buildtree.ProblemReportingBuildActionRunner.run(ProblemReportingBuildActionRunner.java:49) + at org.gradle.launcher.exec.BuildOutcomeReportingBuildActionRunner.run(BuildOutcomeReportingBuildActionRunner.java:71) + at org.gradle.tooling.internal.provider.FileSystemWatchingBuildActionRunner.run(FileSystemWatchingBuildActionRunner.java:135) + at org.gradle.launcher.exec.BuildCompletionNotifyingBuildActionRunner.run(BuildCompletionNotifyingBuildActionRunner.java:41) + at org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.lambda$execute$0(RootBuildLifecycleBuildActionExecutor.java:54) + at org.gradle.composite.internal.DefaultRootBuildState.run(DefaultRootBuildState.java:130) + at org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.execute(RootBuildLifecycleBuildActionExecutor.java:54) + at org.gradle.internal.buildtree.InitDeprecationLoggingActionExecutor.execute(InitDeprecationLoggingActionExecutor.java:62) + at org.gradle.internal.buildtree.InitProblems.execute(InitProblems.java:36) + at org.gradle.internal.buildtree.DefaultBuildTreeContext.execute(DefaultBuildTreeContext.java:40) + at org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.lambda$execute$0(BuildTreeLifecycleBuildActionExecutor.java:71) + at org.gradle.internal.buildtree.BuildTreeState.run(BuildTreeState.java:60) + at org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.execute(BuildTreeLifecycleBuildActionExecutor.java:71) + at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$2.call(RunAsBuildOperationBuildActionExecutor.java:67) + at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$2.call(RunAsBuildOperationBuildActionExecutor.java:63) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) + at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor.execute(RunAsBuildOperationBuildActionExecutor.java:63) + at org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.lambda$execute$0(RunAsWorkerThreadBuildActionExecutor.java:36) + at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) + at org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.execute(RunAsWorkerThreadBuildActionExecutor.java:36) + at org.gradle.tooling.internal.provider.continuous.ContinuousBuildActionExecutor.execute(ContinuousBuildActionExecutor.java:110) + at org.gradle.tooling.internal.provider.SubscribableBuildActionExecutor.execute(SubscribableBuildActionExecutor.java:64) + at org.gradle.internal.session.DefaultBuildSessionContext.execute(DefaultBuildSessionContext.java:46) + at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor$ActionImpl.apply(BuildSessionLifecycleBuildActionExecutor.java:92) + at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor$ActionImpl.apply(BuildSessionLifecycleBuildActionExecutor.java:80) + at org.gradle.internal.session.BuildSessionState.run(BuildSessionState.java:73) + at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor.execute(BuildSessionLifecycleBuildActionExecutor.java:62) + at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor.execute(BuildSessionLifecycleBuildActionExecutor.java:41) + at org.gradle.internal.buildprocess.execution.StartParamsValidatingActionExecutor.execute(StartParamsValidatingActionExecutor.java:64) + at org.gradle.internal.buildprocess.execution.StartParamsValidatingActionExecutor.execute(StartParamsValidatingActionExecutor.java:32) + at org.gradle.internal.buildprocess.execution.SessionFailureReportingActionExecutor.execute(SessionFailureReportingActionExecutor.java:51) + at org.gradle.internal.buildprocess.execution.SessionFailureReportingActionExecutor.execute(SessionFailureReportingActionExecutor.java:39) + at org.gradle.internal.buildprocess.execution.SetupLoggingActionExecutor.execute(SetupLoggingActionExecutor.java:47) + at org.gradle.internal.buildprocess.execution.SetupLoggingActionExecutor.execute(SetupLoggingActionExecutor.java:31) + at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:70) + at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:39) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:29) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:35) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.ForwardClientInput.lambda$execute$0(ForwardClientInput.java:40) + at org.gradle.internal.daemon.clientinput.ClientInputForwarder.forwardInput(ClientInputForwarder.java:80) + at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:37) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.LogAndCheckHealth.execute(LogAndCheckHealth.java:53) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:63) + at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:84) + at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37) + at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104) + at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:52) + at org.gradle.launcher.daemon.server.DaemonStateCoordinator.lambda$runCommand$0(DaemonStateCoordinator.java:321) + at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) + at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) + + +BUILD FAILED in 30s +2026-08-05T00:21:25.537+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] The daemon has finished executing the build. +2026-08-05T00:21:25.555+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] daemon stop has been requested. Sleeping until state changes. +2026-08-05T00:21:25.555+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Command execution: finished waiting for DaemonCommandExecution[command = Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem}, connection = DefaultDaemonConnection: socket connection from /127.0.0.1:50340 to /127.0.0.1:50342]. Result org.gradle.launcher.daemon.server.DaemonStateCoordinator@6f9cb928 with state StopRequested +2026-08-05T00:21:25.556+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Command execution: completed DaemonCommandExecution[command = Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem}, connection = DefaultDaemonConnection: socket connection from /127.0.0.1:50340 to /127.0.0.1:50342] +2026-08-05T00:21:25.556+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] resetting idle timer +2026-08-05T00:21:25.556+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Marking daemon stopped due to command completed and stop requested. The daemon is not running a build +2026-08-05T00:21:25.556+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] daemon has stopped. +2026-08-05T00:21:25.556+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.ReturnResult] Daemon is dispatching the build result: Success[value=org.gradle.launcher.exec.BuildActionResult@5fd92a48] +2026-08-05T00:21:25.557+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 77: dispatching Success[value=org.gradle.launcher.exec.BuildActionResult@5fd92a48] +2026-08-05T00:21:25.557+0800 [DEBUG] [org.gradle.launcher.daemon.server.Daemon] stop() called on daemon +2026-08-05T00:21:25.558+0800 [INFO] [org.gradle.launcher.daemon.server.Daemon] Stop requested. Daemon is removing its presence from the registry... +2026-08-05T00:21:25.558+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Removing our presence to clients, eg. removing this address from the registry: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:25.558+0800 [DEBUG] [org.gradle.launcher.daemon.registry.PersistentDaemonRegistry] Removing daemon address: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:25.561+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on daemon addresses registry. +2026-08-05T00:21:25.562+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry. +2026-08-05T00:21:25.563+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry. +2026-08-05T00:21:25.564+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Address removed from registry. +2026-08-05T00:21:25.581+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.CleanUpVirtualFileSystemAfterBuild] Cleaning virtual file system after build finished +2026-08-05T00:21:25.582+0800 [DEBUG] [org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler] Finishing executing command: Build{id=8f003848-cb45-44c8-a349-abd959060618, currentDir=D:\WORK\SELF\willSystem\willSystem} +2026-08-05T00:21:25.624+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 78: received class org.gradle.launcher.daemon.protocol.CloseInput +2026-08-05T00:21:25.625+0800 [DEBUG] [org.gradle.launcher.daemon.server.DefaultDaemonConnection] thread 78: Received IO message from client: org.gradle.launcher.daemon.protocol.CloseInput@517ed5dc +2026-08-05T00:21:25.625+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 78: received class org.gradle.launcher.daemon.protocol.Finished +2026-08-05T00:21:25.625+0800 [DEBUG] [org.gradle.launcher.daemon.server.DefaultDaemonConnection] thread 78: Received non-IO message from client: org.gradle.launcher.daemon.protocol.Finished@31144112 +2026-08-05T00:21:25.626+0800 [DEBUG] [org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler] Received finished message: org.gradle.launcher.daemon.protocol.Finished@31144112 +2026-08-05T00:21:25.626+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 77: stopping connection +2026-08-05T00:21:25.628+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.SocketConnection] Discarding EOFException: java.io.EOFException +2026-08-05T00:21:25.628+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 78: received null +2026-08-05T00:21:25.628+0800 [DEBUG] [org.gradle.launcher.daemon.server.DefaultDaemonConnection] thread 78: Received end-of-input from client. +2026-08-05T00:21:25.628+0800 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 77: stopping connection +2026-08-05T00:21:25.630+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for jars (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\jars-9) +2026-08-05T00:21:25.630+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on jars (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\jars-9). +2026-08-05T00:21:25.633+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for file content cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\fileContent) +2026-08-05T00:21:25.633+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on file content cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\fileContent). +2026-08-05T00:21:25.634+0800 [DEBUG] [org.gradle.internal.watch.vfs.impl.WatchingVirtualFileSystem] Closing VFS, dropping state +2026-08-05T00:21:25.636+0800 [DEBUG] [org.gradle.internal.watch.registry.impl.DefaultFileWatcherRegistry] Finished listening to file system change events +2026-08-05T00:21:25.636+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Cache Generated Gradle JARs cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\generated-gradle-jars) was closed 0 times. +2026-08-05T00:21:25.636+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for artifact cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\modules-2) +2026-08-05T00:21:25.637+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Closing cache module-metadata.bin (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\modules-2\metadata-2.107\module-metadata.bin) +2026-08-05T00:21:25.637+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Closing cache resource-at-url.bin (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\modules-2\metadata-2.107\resource-at-url.bin) +2026-08-05T00:21:25.638+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on artifact cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\modules-2). +2026-08-05T00:21:25.641+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Cache groovy-dsl (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\groovy-dsl) was closed 0 times. +2026-08-05T00:21:25.642+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Cache Artifact transforms cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\transforms) was closed 0 times. +2026-08-05T00:21:25.643+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for journal cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\journal-1) +2026-08-05T00:21:25.643+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Closing cache file-access.bin (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\journal-1\file-access.bin) +2026-08-05T00:21:25.644+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on journal cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\journal-1). +2026-08-05T00:21:25.644+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for file hash cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\fileHashes) +2026-08-05T00:21:25.645+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Closing cache fileHashes.bin (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\fileHashes\fileHashes.bin) +2026-08-05T00:21:25.646+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on file hash cache (D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches\8.13\fileHashes). +2026-08-05T00:21:25.647+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Deleting unused version-specific caches in D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches' started +2026-08-05T00:21:25.667+0800 [DEBUG] [org.gradle.cache.internal.VersionSpecificCacheCleanupAction] Processed version-specific caches at D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches for cleanup in 0.018 secs +2026-08-05T00:21:25.667+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Deleting unused version-specific caches in D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches' +2026-08-05T00:21:25.667+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Deleting unused version-specific caches in D:\WORK\SELF\willSystem\willSystem\.gradle-codex\caches' completed +2026-08-05T00:21:25.669+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Deleting unused Gradle distributions in D:\WORK\SELF\willSystem\willSystem\.gradle-codex\wrapper\dists' started +2026-08-05T00:21:25.670+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Deleting unused Gradle distributions in D:\WORK\SELF\willSystem\willSystem\.gradle-codex\wrapper\dists' +2026-08-05T00:21:25.671+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Deleting unused Gradle distributions in D:\WORK\SELF\willSystem\willSystem\.gradle-codex\wrapper\dists' completed +2026-08-05T00:21:25.671+0800 [DEBUG] [org.gradle.launcher.daemon.server.Daemon] stop() called on daemon +2026-08-05T00:21:25.671+0800 [INFO] [org.gradle.launcher.daemon.server.Daemon] Stop requested. Daemon is removing its presence from the registry... +2026-08-05T00:21:25.671+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Removing our presence to clients, eg. removing this address from the registry: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:25.672+0800 [DEBUG] [org.gradle.launcher.daemon.registry.PersistentDaemonRegistry] Removing daemon address: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:25.674+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on daemon addresses registry. +2026-08-05T00:21:25.674+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry. +2026-08-05T00:21:25.676+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry. +2026-08-05T00:21:25.677+0800 [DEBUG] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Address removed from registry. +2026-08-05T00:21:25.678+0800 [DEBUG] [org.gradle.cache.internal.locklistener.DefaultFileLockContentionHandler] File lock listener thread completed. +2026-08-05T00:21:25.680+0800 [DEBUG] [java.lang.Runtime] Runtime.exit() called with status: 0 +java.lang.Throwable: Runtime.exit(0) + at java.base/java.lang.Shutdown.logRuntimeExit(Shutdown.java:179) + at java.base/java.lang.Shutdown.exit(Shutdown.java:160) + at java.base/java.lang.Runtime.exit(Runtime.java:188) + at java.base/java.lang.System.exit(System.java:1920) + at org.gradle.launcher.bootstrap.ProcessCompleter.complete(ProcessCompleter.java:21) + at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:61) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:72) + at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:39) + at org.gradle.launcher.daemon.bootstrap.GradleDaemon.main(GradleDaemon.java:22) +Daemon vm is shutting down... The daemon has exited normally or was terminated in response to a user interrupt. +2026-08-05T00:21:25.682+0800 [DEBUG] [org.gradle.launcher.daemon.registry.PersistentDaemonRegistry] Removing daemon address: [395e3ed3-d3f4-4c92-b3c0-52cf4a71fc27 port:50340, addresses:[localhost/127.0.0.1]] +2026-08-05T00:21:25.683+0800 [DEBUG] [org.gradle.launcher.daemon.server.Daemon] VM shutdown hook was unable to remove the daemon address from the registry. It will be cleaned up later. +java.lang.IllegalStateException: Cannot start managing file contention because this handler has been closed. + at org.gradle.cache.internal.locklistener.DefaultFileLockContentionHandler.assertNotStopped(DefaultFileLockContentionHandler.java:248) + at org.gradle.cache.internal.locklistener.DefaultFileLockContentionHandler.reservePort(DefaultFileLockContentionHandler.java:286) + at org.gradle.cache.internal.DefaultFileLockManager.lock(DefaultFileLockManager.java:124) + at org.gradle.cache.internal.DefaultFileLockManager.lock(DefaultFileLockManager.java:106) + at org.gradle.cache.internal.DefaultFileLockManager.lock(DefaultFileLockManager.java:101) + at org.gradle.cache.internal.OnDemandFileAccess.updateFile(OnDemandFileAccess.java:51) + at org.gradle.cache.internal.FileBackedObjectHolder.update(FileBackedObjectHolder.java:77) + at org.gradle.cache.internal.FileIntegrityViolationSuppressingObjectHolderDecorator.lambda$update$0(FileIntegrityViolationSuppressingObjectHolderDecorator.java:48) + at org.gradle.cache.internal.FileIntegrityViolationSuppressingObjectHolderDecorator.doUpdate(FileIntegrityViolationSuppressingObjectHolderDecorator.java:58) + at org.gradle.cache.internal.FileIntegrityViolationSuppressingObjectHolderDecorator.update(FileIntegrityViolationSuppressingObjectHolderDecorator.java:48) + at org.gradle.launcher.daemon.registry.PersistentDaemonRegistry.remove(PersistentDaemonRegistry.java:133) + at org.gradle.launcher.daemon.server.Daemon$1.run(Daemon.java:133) + at java.base/java.lang.Thread.run(Thread.java:1583) diff --git a/.gradle-codex/daemon/8.13/registry.bin b/.gradle-codex/daemon/8.13/registry.bin new file mode 100644 index 0000000..22fd28d Binary files /dev/null and b/.gradle-codex/daemon/8.13/registry.bin differ diff --git a/.gradle-codex/daemon/8.13/registry.bin.lock b/.gradle-codex/daemon/8.13/registry.bin.lock new file mode 100644 index 0000000..3cd8904 Binary files /dev/null and b/.gradle-codex/daemon/8.13/registry.bin.lock differ diff --git a/.gradle-codex/daemon/CACHEDIR.TAG b/.gradle-codex/daemon/CACHEDIR.TAG new file mode 100644 index 0000000..c8907d7 --- /dev/null +++ b/.gradle-codex/daemon/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by Gradle. +# For information about cache directory tags, see: +# https://bford.info/cachedir/ \ No newline at end of file diff --git a/.gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll b/.gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll new file mode 100644 index 0000000..1fb92af Binary files /dev/null and b/.gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll differ diff --git a/.gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll.lock b/.gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll.lock new file mode 100644 index 0000000..6b2aaa7 --- /dev/null +++ b/.gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll.lock @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll b/.gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll new file mode 100644 index 0000000..0b2795a Binary files /dev/null and b/.gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll differ diff --git a/.gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll.lock b/.gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll.lock new file mode 100644 index 0000000..6b2aaa7 --- /dev/null +++ b/.gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll.lock @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.gradle-codex/native/jansi/1.18/windows64/jansi.dll b/.gradle-codex/native/jansi/1.18/windows64/jansi.dll new file mode 100644 index 0000000..cfe23c9 Binary files /dev/null and b/.gradle-codex/native/jansi/1.18/windows64/jansi.dll differ diff --git a/.gradle-codex/notifications/8.13/release-features.rendered b/.gradle-codex/notifications/8.13/release-features.rendered new file mode 100644 index 0000000..e69de29 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..8a9b124 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..4161d33 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..32cf4db --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d54d31b --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/application.properties b/application.properties new file mode 100644 index 0000000..5f69135 --- /dev/null +++ b/application.properties @@ -0,0 +1,2 @@ +logging.level.org.springframework.context.annotation=DEBUG +logging.level.org.springframework.beans.factory=DEBUG \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..616b3d0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'java' + id 'io.spring.dependency-management' version '1.1.7' apply false +} +allprojects { + group = 'com.will' + version = '1.0-SNAPSHOT' + + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +subprojects { + apply plugin: 'java' // 明确为每个子项目应用 java 插件 + apply plugin: 'io.spring.dependency-management' // 复用上面声明的版本 + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } + compileJava { + options.compilerArgs << '-parameters' + } + dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:3.5.7" + mavenBom "org.springframework.ai:spring-ai-bom:1.0.0" + mavenBom "org.bsc.langgraph4j:langgraph4j-bom:1.8.19" + } + } + + dependencies { + implementation 'org.springframework:spring-context' + implementation 'org.springframework:spring-aop' + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.boot:spring-boot-starter-json") + implementation 'org.springframework.boot:spring-boot-autoconfigure' + implementation 'org.slf4j:slf4j-api' + + implementation("com.fasterxml.jackson.core:jackson-databind:2.20.1") + implementation("cn.hutool:hutool-all:5.8.41") + + + implementation 'org.aspectj:aspectjweaver:1.9.25' + + implementation("org.apache.commons:commons-pool2:2.12.1") + + implementation enforcedPlatform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' + + } + + + test { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/businessAgent/build.gradle b/businessAgent/build.gradle new file mode 100644 index 0000000..68be67b --- /dev/null +++ b/businessAgent/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(':coreCommon') + implementation project(':coreService') + implementation project(':coreAgent') + implementation project(':coreAuth') + + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.ai:spring-ai-client-chat' + implementation 'org.springframework.ai:spring-ai-starter-mcp-client' + + implementation 'org.bsc.langgraph4j:langgraph4j-core' + implementation 'org.bsc.langgraph4j:langgraph4j-spring-ai' +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenAgentService.java b/businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenAgentService.java new file mode 100644 index 0000000..0428cb3 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenAgentService.java @@ -0,0 +1,90 @@ +package com.will.businessAgent.codegen; + +import com.will.businessAgent.codegen.CodeArtifact; +import com.will.businessAgent.codegen.CodeGenRequest; +import com.will.businessAgent.codegen.CodeGenResult; +import com.will.businessAgent.codegen.CodeGenState; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bsc.langgraph4j.CompiledGraph; +import org.bsc.langgraph4j.NodeOutput; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class BusinessCodeGenAgentService { + + private final CompiledGraph businessCodeGenCompiledGraph; + + public CodeGenResult generate(CodeGenRequest request) { + if (request == null || !StringUtils.hasText(request.getRequirement())) { + throw new IllegalArgumentException("requirement 不能为空"); + } + + String moduleName = StringUtils.hasText(request.getModuleName()) ? request.getModuleName() : "demo"; + String entityName = StringUtils.hasText(request.getEntityName()) + ? request.getEntityName() + : capitalize(moduleName); + + Map inputs = Map.of( + CodeGenState.REQUIREMENT, request.getRequirement(), + CodeGenState.MODULE_NAME, moduleName, + CodeGenState.ENTITY_NAME, entityName + ); + + CodeGenState finalState = null; + for (NodeOutput output : businessCodeGenCompiledGraph.stream(inputs)) { + log.debug("[Agent] node={}", output.node()); + if (output.state() != null) { + finalState = output.state(); + } + } + + if (finalState == null) { + throw new IllegalStateException("Agent 执行失败:未产生最终状态"); + } + + List artifacts = new ArrayList<>(); + addArtifact(artifacts, "entity", entityName + ".java", finalState.entityCode().orElse("")); + addArtifact(artifacts, "mapper", entityName + "Mapper.java", finalState.mapperCode().orElse("")); + addArtifact(artifacts, "dto", entityName + "DTO.java", finalState.dtoCode().orElse("")); + addArtifact(artifacts, "service", entityName + "Service.java", finalState.serviceCode().orElse("")); + addArtifact(artifacts, "controller", "Auth" + entityName + "Controller.java", + finalState.controllerCode().orElse("")); + + return CodeGenResult.builder() + .moduleName(finalState.moduleName().orElse(moduleName)) + .entityName(finalState.entityName().orElse(entityName)) + .plan(finalState.plan().orElse("")) + .summary(finalState.summary().orElse("")) + .mcpContext(finalState.mcpContext().orElse("")) + .artifacts(artifacts) + .build(); + } + + private void addArtifact(List artifacts, String layer, String fileName, String content) { + if (!StringUtils.hasText(content)) { + return; + } + artifacts.add(CodeArtifact.builder() + .layer(layer) + .fileName(fileName) + .content(content) + .build()); + } + + private static String capitalize(String value) { + if (value.length() == 1) { + return value.toUpperCase(); + } + return Character.toUpperCase(value.charAt(0)) + value.substring(1); + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenGraph.java b/businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenGraph.java new file mode 100644 index 0000000..c98dfe9 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenGraph.java @@ -0,0 +1,165 @@ +package com.will.businessAgent.codegen; + +import com.will.coreAgent.framework.mcp.McpContextService; +import com.will.coreAgent.agent.prompt.WillSystemPrompts; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bsc.langgraph4j.CompiledGraph; +import org.bsc.langgraph4j.GraphStateException; +import org.bsc.langgraph4j.StateGraph; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import java.util.Map; + +import static org.bsc.langgraph4j.StateGraph.END; +import static org.bsc.langgraph4j.StateGraph.START; +import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async; + +/** + * LangGraph4j 工作流:解析需求 → MCP 上下文增强 → 生成计划 → 生成各层代码 → 总结。 + * + *
+ * START → init → enrichMcp → plan → generate → summary → END
+ * 
+ */ +@Slf4j +@Configuration +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class BusinessCodeGenGraph { + + private final McpContextService mcpContextService; + + @Bean + public CompiledGraph businessCodeGenCompiledGraph(ChatClient agentChatClient) + throws GraphStateException { + + var initNode = node_async((CodeGenState state) -> { + String module = state.moduleName().filter(StringUtils::hasText).orElse("demo"); + String entity = state.entityName().filter(StringUtils::hasText) + .orElse(capitalize(module)); + log.info("[Agent] init module={}, entity={}", module, entity); + return Map.of( + CodeGenState.MODULE_NAME, module, + CodeGenState.ENTITY_NAME, entity + ); + }); + + var enrichMcpNode = node_async((CodeGenState state) -> { + String context = mcpContextService.buildContextSnippet( + state.requirement().orElse(""), + state.moduleName().orElse("demo")); + if (mcpContextService.isEnabled()) { + log.info("[Agent][MCP] context enriched, tools={}, servers={}", + mcpContextService.listToolNames().size(), + mcpContextService.connectedServerCount()); + } + return Map.of(CodeGenState.MCP_CONTEXT, context); + }); + + var planNode = node_async((CodeGenState state) -> { + String plan = agentChatClient.prompt() + .user(WillSystemPrompts.planPrompt( + state.requirement().orElse(""), + state.moduleName().orElse("demo"), + state.entityName().orElse("Demo"), + state.mcpContext().orElse(""))) + .call() + .content(); + log.info("[Agent] plan generated"); + return Map.of(CodeGenState.PLAN, plan == null ? "" : plan); + }); + + var generateNode = node_async((CodeGenState state) -> { + String module = state.moduleName().orElse("demo"); + String entity = state.entityName().orElse("Demo"); + String plan = state.plan().orElse(""); + String mcpContext = state.mcpContext().orElse(""); + + String entityCode = generateLayer(agentChatClient, "entity", module, entity, plan, mcpContext); + String mapperCode = generateLayer(agentChatClient, "mapper", module, entity, plan, mcpContext); + String dtoCode = generateLayer(agentChatClient, "dto", module, entity, plan, mcpContext); + String serviceCode = generateLayer(agentChatClient, "service", module, entity, plan, mcpContext); + String controllerCode = generateLayer(agentChatClient, "controller", module, entity, plan, mcpContext); + + log.info("[Agent] code layers generated"); + return Map.of( + CodeGenState.ENTITY_CODE, entityCode, + CodeGenState.MAPPER_CODE, mapperCode, + CodeGenState.DTO_CODE, dtoCode, + CodeGenState.SERVICE_CODE, serviceCode, + CodeGenState.CONTROLLER_CODE, controllerCode + ); + }); + + var summaryNode = node_async((CodeGenState state) -> { + String summary = agentChatClient.prompt() + .user(WillSystemPrompts.summaryPrompt( + state.moduleName().orElse("demo"), + state.entityName().orElse("Demo"), + state.plan().orElse(""), + state.mcpContext().orElse(""))) + .call() + .content(); + return Map.of(CodeGenState.SUMMARY, summary == null ? "" : summary); + }); + + StateGraph graph = new StateGraph<>(CodeGenState.SCHEMA, CodeGenState::new) + .addNode("init", initNode) + .addNode("enrichMcp", enrichMcpNode) + .addNode("plan", planNode) + .addNode("generate", generateNode) + .addNode("summary", summaryNode) + .addEdge(START, "init") + .addEdge("init", "enrichMcp") + .addEdge("enrichMcp", "plan") + .addEdge("plan", "generate") + .addEdge("generate", "summary") + .addEdge("summary", END); + + return graph.compile(); + } + + private static String generateLayer( + ChatClient chatClient, + String layer, + String module, + String entity, + String plan, + String mcpContext) { + String content = chatClient.prompt() + .user(WillSystemPrompts.codePrompt(layer, module, entity, plan, mcpContext)) + .call() + .content(); + return stripMarkdownFence(content); + } + + private static String stripMarkdownFence(String content) { + if (content == null) { + return ""; + } + String trimmed = content.trim(); + if (trimmed.startsWith("```")) { + int firstNewline = trimmed.indexOf('\n'); + int lastFence = trimmed.lastIndexOf("```"); + if (firstNewline > 0 && lastFence > firstNewline) { + return trimmed.substring(firstNewline + 1, lastFence).trim(); + } + } + return trimmed; + } + + private static String capitalize(String value) { + if (!StringUtils.hasText(value)) { + return "Demo"; + } + if (value.length() == 1) { + return value.toUpperCase(); + } + return Character.toUpperCase(value.charAt(0)) + value.substring(1); + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeArtifact.java b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeArtifact.java new file mode 100644 index 0000000..1da1e72 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeArtifact.java @@ -0,0 +1,12 @@ +package com.will.businessAgent.codegen; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CodeArtifact { + private String fileName; + private String layer; + private String content; +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenRequest.java b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenRequest.java new file mode 100644 index 0000000..5291713 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenRequest.java @@ -0,0 +1,13 @@ +package com.will.businessAgent.codegen; + +import lombok.Data; + +@Data +public class CodeGenRequest { + /** 自然语言业务需求,如:商品管理,含名称、价格、库存 CRUD */ + private String requirement; + /** 业务模块名(英文小写),如 product、order,默认 demo */ + private String moduleName; + /** 实体类名(PascalCase),如 Product,默认根据 moduleName 推导 */ + private String entityName; +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenResult.java b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenResult.java new file mode 100644 index 0000000..4fbd583 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenResult.java @@ -0,0 +1,17 @@ +package com.will.businessAgent.codegen; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class CodeGenResult { + private String moduleName; + private String entityName; + private String plan; + private String summary; + private String mcpContext; + private List artifacts; +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenState.java b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenState.java new file mode 100644 index 0000000..ed1b6cd --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenState.java @@ -0,0 +1,88 @@ +package com.will.businessAgent.codegen; + +import org.bsc.langgraph4j.state.AgentState; +import org.bsc.langgraph4j.state.Channel; +import org.bsc.langgraph4j.state.Channels; + +import java.util.Map; +import java.util.Optional; + +/** + * LangGraph4j 业务代码生成状态。 + */ +public class CodeGenState extends AgentState { + + public static final String REQUIREMENT = "requirement"; + public static final String MODULE_NAME = "moduleName"; + public static final String ENTITY_NAME = "entityName"; + public static final String PLAN = "plan"; + public static final String ENTITY_CODE = "entityCode"; + public static final String MAPPER_CODE = "mapperCode"; + public static final String DTO_CODE = "dtoCode"; + public static final String SERVICE_CODE = "serviceCode"; + public static final String CONTROLLER_CODE = "controllerCode"; + public static final String SUMMARY = "summary"; + public static final String MCP_CONTEXT = "mcpContext"; + + public static final Map> SCHEMA = Map.ofEntries( + Map.entry(REQUIREMENT, Channels.base(() -> "")), + Map.entry(MODULE_NAME, Channels.base(() -> "")), + Map.entry(ENTITY_NAME, Channels.base(() -> "")), + Map.entry(PLAN, Channels.base(() -> "")), + Map.entry(ENTITY_CODE, Channels.base(() -> "")), + Map.entry(MAPPER_CODE, Channels.base(() -> "")), + Map.entry(DTO_CODE, Channels.base(() -> "")), + Map.entry(SERVICE_CODE, Channels.base(() -> "")), + Map.entry(CONTROLLER_CODE, Channels.base(() -> "")), + Map.entry(SUMMARY, Channels.base(() -> "")), + Map.entry(MCP_CONTEXT, Channels.base(() -> "")) + ); + + public CodeGenState(Map initData) { + super(initData); + } + + public Optional requirement() { + return value(REQUIREMENT); + } + + public Optional moduleName() { + return value(MODULE_NAME); + } + + public Optional entityName() { + return value(ENTITY_NAME); + } + + public Optional plan() { + return value(PLAN); + } + + public Optional entityCode() { + return value(ENTITY_CODE); + } + + public Optional mapperCode() { + return value(MAPPER_CODE); + } + + public Optional dtoCode() { + return value(DTO_CODE); + } + + public Optional serviceCode() { + return value(SERVICE_CODE); + } + + public Optional controllerCode() { + return value(CONTROLLER_CODE); + } + + public Optional summary() { + return value(SUMMARY); + } + + public Optional mcpContext() { + return value(MCP_CONTEXT); + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentAutoConfiguration.java b/businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentAutoConfiguration.java new file mode 100644 index 0000000..1c28d35 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.will.businessAgent.config; + +import com.will.businessAgent.codegen.BusinessCodeGenAgentService; +import com.will.businessAgent.codegen.BusinessCodeGenGraph; +import com.will.businessAgent.controller.AgentCodeController; +import com.will.businessAgent.controller.AgentZentaoController; +import com.will.businessAgent.tool.WillSystemAgentTools; +import com.will.businessAgent.tool.WillSystemOrgTools; +import com.will.businessAgent.tool.ZentaoReportTools; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +@Import({ + BusinessAgentToolConfiguration.class, + BusinessCodeGenGraph.class, + BusinessCodeGenAgentService.class, + WillSystemAgentTools.class, + WillSystemOrgTools.class, + ZentaoReportTools.class, + AgentCodeController.class, + AgentZentaoController.class +}) +public class BusinessAgentAutoConfiguration { +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentToolConfiguration.java b/businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentToolConfiguration.java new file mode 100644 index 0000000..ea4855e --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentToolConfiguration.java @@ -0,0 +1,35 @@ +package com.will.businessAgent.config; + +import com.will.coreAgent.framework.tool.UserContextAwareToolCallbackProvider; +import com.will.businessAgent.tool.WillSystemAgentTools; +import com.will.businessAgent.tool.WillSystemOrgTools; +import com.will.businessAgent.tool.ZentaoReportTools; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class BusinessAgentToolConfiguration { + + @Bean + public ToolCallbackProvider willSystemAgentToolProvider( + WillSystemAgentTools agentTools, + WillSystemOrgTools orgTools, + ObjectProvider zentaoReportTools) { + List tools = new ArrayList<>(); + tools.add(agentTools); + tools.add(orgTools); + zentaoReportTools.ifAvailable(tools::add); + ToolCallbackProvider methodProvider = MethodToolCallbackProvider.builder() + .toolObjects(tools.toArray()) + .build(); + return new UserContextAwareToolCallbackProvider(methodProvider); + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/controller/AgentCodeController.java b/businessAgent/src/main/java/com/will/businessAgent/controller/AgentCodeController.java new file mode 100644 index 0000000..535d3c2 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/controller/AgentCodeController.java @@ -0,0 +1,34 @@ +package com.will.businessAgent.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.businessAgent.codegen.CodeGenRequest; +import com.will.businessAgent.codegen.CodeGenResult; +import com.will.businessAgent.codegen.BusinessCodeGenAgentService; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/agent/code") +@RequiredArgsConstructor +public class AgentCodeController { + + private final ObjectProvider businessCodeGenAgentService; + + @RequirePermission(ApiPermissions.AGENT_CODE_GEN) + @PostMapping("/generate") + public ResponseMessage generate(@RequestBody CodeGenRequest request) { + BusinessCodeGenAgentService service = businessCodeGenAgentService.getIfAvailable(); + if (service == null) { + return ResponseMessageFactory.error( + "AI Agent 未启用,请在 application.yml 中设置 will.agent.enabled=true 并配置大模型"); + } + return ResponseMessageFactory.success(service.generate(request)); + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/controller/AgentZentaoController.java b/businessAgent/src/main/java/com/will/businessAgent/controller/AgentZentaoController.java new file mode 100644 index 0000000..53df956 --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/controller/AgentZentaoController.java @@ -0,0 +1,39 @@ +package com.will.businessAgent.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.businessAgent.tool.ZentaoReportTools; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; + +@RestController +@RequestMapping("/api/agent/zentao") +@RequiredArgsConstructor +public class AgentZentaoController { + + private final ObjectProvider zentaoReportTools; + + @RequirePermission(ApiPermissions.AGENT_LIST) + @GetMapping("/tso/effort/week") + public ResponseMessage zentaoTsoEffortThisWeek() { + ZentaoReportTools tools = zentaoReportTools.getIfAvailable(); + if (tools == null) { + return ResponseMessageFactory.error("禅道工具未启用"); + } + LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai")); + LocalDate weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + return ResponseMessageFactory.success( + tools.getZentaoGroupTaskEffortStats(weekStart.toString(), weekEnd.toString(), "TSO")); + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemAgentTools.java b/businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemAgentTools.java new file mode 100644 index 0000000..34b137b --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemAgentTools.java @@ -0,0 +1,29 @@ +package com.will.businessAgent.tool; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * 智能体内置工具(非 MCP 进程,直接注册到 ChatClient)。 + */ +@Component +public class WillSystemAgentTools { + + @Tool(description = "获取 willSystem 项目模块说明") + public String getWillSystemModuleInfo( + @ToolParam(description = "模块名:user、department、role、menu、agent") String module) { + if (!StringUtils.hasText(module)) { + return "请提供模块名"; + } + return switch (module.trim().toLowerCase()) { + case "user" -> "用户管理:/api/sys/user,权限 sys:user:list|save|delete"; + case "department", "dept" -> "部门管理:/api/sys/department,权限 sys:department:*"; + case "role" -> "角色管理:/api/sys/role,权限 sys:role:*"; + case "menu" -> "菜单管理:/api/sys/menu,权限 sys:menu:*"; + case "agent" -> "AI 智能体:/api/agent/chat、/api/agent/code/generate"; + default -> "未知模块:" + module + "。可选:user、department、role、menu、agent"; + }; + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemOrgTools.java b/businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemOrgTools.java new file mode 100644 index 0000000..63f946a --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemOrgTools.java @@ -0,0 +1,212 @@ +package com.will.businessAgent.tool; + +import com.will.coreAgent.agent.context.AgentOperatorContext; +import com.will.coreCommon.BaseListDTO; +import com.will.coreCommon.BasePage; +import com.will.coreCommon.context.UserContext; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreService.dto.DepartmentDTO; +import com.will.coreService.dto.UserDTO; +import com.will.coreService.dto.UserSaveDTO; +import com.will.coreService.service.DepartmentService; +import com.will.coreService.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * willSystem 组织架构查询工具:部门 / 用户(遵循数据权限)。 + * 通过 ChatClient ToolCallback 暴露给大模型,等价于业务 MCP 能力。 + */ +@Component +@RequiredArgsConstructor +public class WillSystemOrgTools { + + private final DepartmentService departmentService; + private final UserService userService; + + @Tool(description = "查询 willSystem 部门树(含编码、名称、状态)。用于了解组织架构。") + public String listDepartments() { + ensureUserContext(); + ResponseMessage> result = departmentService.getDepartmentListDto(); + if (result.hasError()) { + return "查询部门失败:" + result.getMessage(); + } + List list = result.getData() == null ? List.of() : result.getData().getList(); + if (list == null || list.isEmpty()) { + return "当前用户可见部门为空"; + } + StringBuilder sb = new StringBuilder("部门树(共 ").append(countNodes(list)).append(" 个):\n"); + appendDeptTree(sb, list, 0); + return sb.toString(); + } + + @Tool(description = "按部门名称关键字搜索部门,返回匹配的部门列表") + public String searchDepartments( + @ToolParam(description = "部门名称关键字,例如:研发、销售") String departmentName) { + ensureUserContext(); + if (!StringUtils.hasText(departmentName)) { + return "请提供部门名称关键字"; + } + DepartmentDTO condition = new DepartmentDTO(); + condition.setDepartmentName(departmentName.trim()); + ResponseMessage> result = + departmentService.getDepartmentPageDto(condition, 1, 50); + if (result.hasError()) { + return "搜索部门失败:" + result.getMessage(); + } + BasePage page = result.getData(); + if (page == null || page.getList() == null || page.getList().isEmpty()) { + return "未找到名称包含「" + departmentName + "」的部门"; + } + StringBuilder sb = new StringBuilder("匹配部门(").append(page.getTotal()).append("):\n"); + appendDeptTree(sb, page.getList(), 0); + return sb.toString(); + } + + @Tool(description = "按部门 ID 分页查询该部门及子部门下的用户列表") + public String listUsersByDepartment( + @ToolParam(description = "部门 ID") String departmentId, + @ToolParam(description = "页码,从 1 开始,默认 1", required = false) Integer pageIndex, + @ToolParam(description = "每页条数,默认 10,最大 50", required = false) Integer pageSize) { + ensureUserContext(); + if (!StringUtils.hasText(departmentId)) { + return "请提供部门 ID(可先调用 listDepartments 或 searchDepartments 获取)"; + } + int page = pageIndex == null || pageIndex < 1 ? 1 : pageIndex; + int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 50); + ResponseMessage> result = + departmentService.getPageUsersByDepartmentId(departmentId.trim(), page, size); + if (result.hasError()) { + return "查询部门用户失败:" + result.getMessage(); + } + return formatUserPage(result.getData(), "部门 " + departmentId + " 用户"); + } + + @Tool(description = "按登录账号或姓名关键字分页搜索 willSystem 用户;用户询问用户信息、人员信息、账号列表时优先调用") + public String searchUsers( + @ToolParam(description = "登录账号或姓名关键字,可为空表示查全部可见用户", required = false) String account, + @ToolParam(description = "页码,从 1 开始,默认 1", required = false) Integer pageIndex, + @ToolParam(description = "每页条数,默认 10,最大 50", required = false) Integer pageSize) { + ensureUserContext(); + int page = pageIndex == null || pageIndex < 1 ? 1 : pageIndex; + int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 50); + String keyword = StringUtils.hasText(account) ? account.trim() : null; + ResponseMessage> result = userService.page(keyword, page, size); + if (result.hasError()) { + return "搜索用户失败:" + result.getMessage(); + } + String title = keyword == null ? "用户列表" : "账号或姓名包含「" + keyword + "」的用户"; + return formatUserPage(result.getData(), title); + } + + @Tool(description = "按用户 ID 查询用户详情(账号、姓名、邮箱、角色等,不含密码)") + public String getUserDetail( + @ToolParam(description = "用户 ID") String userId) { + ensureUserContext(); + if (!StringUtils.hasText(userId)) { + return "请提供用户 ID(可先调用 searchUsers 获取)"; + } + ResponseMessage result = userService.detail(userId.trim()); + if (result.hasError()) { + return "查询用户详情失败:" + result.getMessage(); + } + UserSaveDTO user = result.getData(); + if (user == null) { + return "用户不存在"; + } + return """ + 用户详情: + - ID:%s + - 账号:%s + - 姓名:%s + - 邮箱:%s + - 手机:%s + - 状态:%s + - 角色ID:%s + """.formatted( + nullToDash(user.getId()), + nullToDash(user.getAccount()), + nullToDash(user.getUserName()), + nullToDash(user.getEmail()), + user.getPhoneNumber() == null ? "-" : user.getPhoneNumber(), + Boolean.FALSE.equals(user.getStatus()) ? "禁用" : "启用", + user.getRoleIds() == null || user.getRoleIds().isEmpty() + ? "-" + : String.join(", ", user.getRoleIds()) + ); + } + + private void ensureUserContext() { + String userId = UserContext.getCurrentUserId(); + if (!StringUtils.hasText(userId)) { + userId = AgentOperatorContext.get(); + if (StringUtils.hasText(userId)) { + UserContext.setCurrentUserId(userId); + } + } + if (!StringUtils.hasText(userId)) { + throw new IllegalStateException("未获取到当前登录用户,请重新登录后再试"); + } + } + + private static String formatUserPage(BasePage page, String title) { + if (page == null || page.getList() == null || page.getList().isEmpty()) { + return title + ":无数据"; + } + StringBuilder sb = new StringBuilder(title) + .append("(共 ").append(page.getTotal()).append(" 人):\n"); + for (UserDTO user : page.getList()) { + sb.append("- ID=").append(user.getId()) + .append(" | 账号=").append(nullToDash(user.getAccount())) + .append(" | 姓名=").append(nullToDash(user.getUserName())) + .append(" | 状态=").append(Boolean.FALSE.equals(user.getStatus()) ? "禁用" : "启用") + .append(" | 邮箱=").append(nullToDash(user.getEmail())) + .append('\n'); + } + return sb.toString(); + } + + private static void appendDeptTree(StringBuilder sb, List nodes, int depth) { + if (nodes == null) { + return; + } + String indent = " ".repeat(Math.max(0, depth)); + for (DepartmentDTO dept : nodes) { + sb.append(indent) + .append("- ID=").append(nullToDash(dept.getId())) + .append(" | 名称=").append(nullToDash(dept.getDepartmentName())) + .append(" | 编码=").append(nullToDash(dept.getDepartmentCode())) + .append(" | 状态=").append(dept.getStatus() == null ? "-" : dept.getStatus()) + .append('\n'); + if (dept.getChildren() != null && !dept.getChildren().isEmpty()) { + appendDeptTree(sb, dept.getChildren(), depth + 1); + } + } + } + + private static int countNodes(List nodes) { + if (nodes == null || nodes.isEmpty()) { + return 0; + } + int count = 0; + List queue = new ArrayList<>(nodes); + while (!queue.isEmpty()) { + DepartmentDTO node = queue.remove(queue.size() - 1); + count++; + if (node.getChildren() != null) { + queue.addAll(node.getChildren()); + } + } + return count; + } + + private static String nullToDash(String value) { + return StringUtils.hasText(value) ? value : "-"; + } +} diff --git a/businessAgent/src/main/java/com/will/businessAgent/tool/ZentaoReportTools.java b/businessAgent/src/main/java/com/will/businessAgent/tool/ZentaoReportTools.java new file mode 100644 index 0000000..ebc674a --- /dev/null +++ b/businessAgent/src/main/java/com/will/businessAgent/tool/ZentaoReportTools.java @@ -0,0 +1,1003 @@ +package com.will.businessAgent.tool; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.zaxxer.hikari.HikariDataSource; +import jakarta.annotation.PostConstruct; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 禅道报表查询工具。只做固定聚合查询,不执行模型传入的任意 SQL。 + */ +@Component +@Slf4j +@ConditionalOnBean(name = "zentaoJdbcTemplate") +public class ZentaoReportTools { + + private final JdbcTemplate zentaoJdbcTemplate; + + public ZentaoReportTools(@Qualifier("zentaoJdbcTemplate") JdbcTemplate zentaoJdbcTemplate) { + this.zentaoJdbcTemplate = zentaoJdbcTemplate; + } + + @PostConstruct + public void logZentaoJdbcTemplate() { + Object dataSource = zentaoJdbcTemplate.getDataSource(); + if (dataSource instanceof HikariDataSource hikariDataSource) { + log.info("[Agent][Zentao] report tools bound jdbcUrl={}, username={}", + hikariDataSource.getJdbcUrl(), hikariDataSource.getUsername()); + } else { + log.info("[Agent][Zentao] report tools bound datasource={}", + dataSource == null ? "null" : dataSource.getClass().getName()); + } + } + + @Tool(description = "查询禅道用户总数和启用用户数,用于人员规模汇报") + public String getZentaoUserSummary() { + Long total = queryLong("SELECT COUNT(1) FROM zt_user WHERE deleted = '0'"); + Long active = queryLong("SELECT COUNT(1) FROM zt_user WHERE deleted = '0' AND locked is null"); + return """ + 禅道用户概览: + - 用户总数:%s + - 启用用户数:%s + """.formatted(value(total), value(active)); + } + + @Tool(description = "按产品名称或产品 ID 查询禅道 Bug 汇总,包含状态、严重程度、优先级分布") + public String getZentaoBugSummary( + @ToolParam(description = "产品名称关键字或产品 ID,可为空表示全部产品", required = false) String product) { + SqlFilter productFilter = resolveProductFilter(product); + String where = "b.deleted = '0'" + productFilter.clause(); + Object[] args = productFilter.params().toArray(); + + Long total = queryLong("SELECT COUNT(1) FROM zt_bug b WHERE " + where, args); + List> byStatus = queryList( + "SELECT b.status name, COUNT(1) count FROM zt_bug b WHERE " + where + + " GROUP BY b.status ORDER BY count DESC", + args); + List> bySeverity = queryList( + "SELECT b.severity name, COUNT(1) count FROM zt_bug b WHERE " + where + + " GROUP BY b.severity ORDER BY b.severity", + args); + List> byPri = queryList( + "SELECT b.pri name, COUNT(1) count FROM zt_bug b WHERE " + where + + " GROUP BY b.pri ORDER BY b.pri", + args); + + return """ + 禅道 Bug 汇总%s: + - Bug 总数:%s + - 按状态:%s + - 按严重程度:%s + - 按优先级:%s + """.formatted(scopeText(product), value(total), formatRows(byStatus), formatRows(bySeverity), formatRows(byPri)); + } + + @Tool(description = "按项目名称或项目 ID 查询禅道任务汇总,包含任务状态分布") + public String getZentaoTaskSummary( + @ToolParam(description = "项目名称关键字或项目 ID,可为空表示全部项目", required = false) String project) { + SqlFilter projectFilter = resolveProjectFilter(project); + String where = "t.deleted = '0'" + projectFilter.clause(); + Object[] args = projectFilter.params().toArray(); + + Long total = queryLong("SELECT COUNT(1) FROM zt_task t WHERE " + where, args); + List> byStatus = queryList( + "SELECT t.status name, COUNT(1) count FROM zt_task t WHERE " + where + + " GROUP BY t.status ORDER BY count DESC", + args); + List> byAssignedTo = queryList( + "SELECT t.assignedTo name, COUNT(1) count FROM zt_task t WHERE " + where + + " GROUP BY t.assignedTo ORDER BY count DESC LIMIT 10", + args); + + return """ + 禅道任务汇总%s: + - 任务总数:%s + - 按状态:%s + - 负责人 TOP10:%s + """.formatted(scopeText(project), value(total), formatRows(byStatus), formatRows(byAssignedTo)); + } + + @Tool(description = "生成禅道日报概览,汇总用户、Bug、任务核心指标") + public String getZentaoDailyReport() { + return getZentaoUserSummary() + "\n" + + getZentaoBugSummary(null) + "\n" + + getZentaoTaskSummary(null); + } + + @Tool(description = "查询禅道 TSO 用户组成员,返回账号和真实姓名") + public String listZentaoTsoUsers() { + List> users = queryList(""" + SELECT u.account, u.realname + FROM zt_user u + INNER JOIN zt_usergroup ug ON u.account = ug.account + INNER JOIN zt_group g ON g.id = ug.`group` + WHERE g.`name` = 'TSO' AND u.deleted = '0' + ORDER BY u.account + """); + if (users.isEmpty()) { + return "禅道 TSO 用户组:无用户"; + } + String rows = users.stream() + .map(row -> "- 账号=%s | 姓名=%s".formatted( + row.getOrDefault("account", "-"), + row.getOrDefault("realname", "-"))) + .collect(Collectors.joining("\n")); + return "禅道 TSO 用户组(共 %s 人):\n%s".formatted(users.size(), rows); + } + + @Tool(description = "按开始日期和结束日期统计禅道任务工时,支持指定单个或多个用户账号/姓名,返回每日、用户、任务维度的消耗工时") + public String getZentaoTaskEffortStats( + @ToolParam(description = "开始日期,格式 yyyy-MM-dd,按禅道工时开始日期/日期过滤") String startDate, + @ToolParam(description = "结束日期,格式 yyyy-MM-dd,按禅道工时开始日期/日期过滤") String endDate, + @ToolParam(description = "禅道用户账号或姓名,支持单个或多个,多个用英文逗号分隔;为空表示全部用户", required = false) String accounts) { + LocalDate start = parseDate(startDate); + LocalDate end = parseDate(endDate); + if (start == null || end == null) { + return "禅道任务工时统计:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01"; + } + if (start.isAfter(end)) { + return "禅道任务工时统计:开始日期不能晚于结束日期"; + } + + List accountList = parseAccounts(accounts); + List params = new ArrayList<>(); + params.add(start.toString()); + params.add(end.plusDays(1).toString()); + + String accountFilter = ""; + if (!accountList.isEmpty()) { + accountFilter = buildUserIdentityFilter("e.account", accountList, params); + } + + List> rows = queryList(""" + SELECT e.`date` effortDate, + e.account account, + COALESCE(u.realname, e.account) realname, + e.objectID taskId, + COALESCE(t.name, CONCAT('任务#', e.objectID)) taskName, + ROUND(SUM(e.consumed), 2) consumed + FROM zt_effort e + LEFT JOIN zt_task t ON t.id = e.objectID AND e.objectType = 'task' + LEFT JOIN zt_user u ON u.account = e.account + WHERE e.objectType = 'task' + AND e.`date` >= ? + AND e.`date` <= ? + """ + accountFilter + """ + + GROUP BY e.`date`, e.account, u.realname, e.objectID, t.name + ORDER BY e.`date` ASC, e.account ASC, e.objectID ASC + LIMIT 500 + """, params.toArray()); + + if (isQueryFailed(rows)) { + return "禅道任务工时统计:查询失败," + rows.get(0).get("count"); + } + if (rows.isEmpty()) { + return "禅道任务工时统计:无数据"; + } + + double totalConsumed = rows.stream() + .map(row -> row.get("consumed")) + .filter(Number.class::isInstance) + .map(Number.class::cast) + .mapToDouble(Number::doubleValue) + .sum(); + String details = rows.stream() + .map(row -> "- 日期=%s | 账号=%s | 姓名=%s | 任务=%s %s | 消耗工时=%s".formatted( + row.getOrDefault("effortDate", "-"), + row.getOrDefault("account", "-"), + row.getOrDefault("realname", "-"), + row.getOrDefault("taskId", "-"), + row.getOrDefault("taskName", "-"), + row.getOrDefault("consumed", "-"))) + .collect(Collectors.joining("\n")); + + return """ + 禅道任务工时统计: + - 时间范围:%s 至 %s + - 用户范围:%s + - 记录数:%s + - 总消耗工时:%.2f + 明细: + %s + """.formatted(start, end, accountList.isEmpty() ? "全部用户" : String.join(",", accountList), + rows.size(), totalConsumed, details); + } + + @Tool(description = "按开始日期和结束日期统计禅道指定用户组的任务工时,例如 TSO 组本周工时统计") + public String getZentaoGroupTaskEffortStats( + @ToolParam(description = "开始日期,格式 yyyy-MM-dd") String startDate, + @ToolParam(description = "结束日期,格式 yyyy-MM-dd") String endDate, + @ToolParam(description = "禅道用户组名称,例如 TSO") String groupName) { + LocalDate start = parseDate(startDate); + LocalDate end = parseDate(endDate); + if (start == null || end == null) { + return "禅道用户组任务工时统计:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01"; + } + if (start.isAfter(end)) { + return "禅道用户组任务工时统计:开始日期不能晚于结束日期"; + } + String actualGroupName = StringUtils.hasText(groupName) ? groupName.trim() : "TSO"; + List params = List.of(start.toString(), end.toString(), actualGroupName); + + String groupFilter = """ + AND e.account IN ( + SELECT ug.account + FROM zt_usergroup ug + INNER JOIN zt_group g ON g.id = ug.`group` + INNER JOIN zt_user u2 ON u2.account = ug.account + WHERE g.`name` = ? AND u2.deleted = '0' + ) + """; + List> rows = queryList(""" + SELECT e.`date` effortDate, + e.account account, + COALESCE(u.realname, e.account) realname, + e.objectID taskId, + COALESCE(t.name, CONCAT('任务#', e.objectID)) taskName, + ROUND(SUM(e.consumed), 2) consumed + FROM zt_effort e + LEFT JOIN zt_task t ON t.id = e.objectID AND e.objectType = 'task' + LEFT JOIN zt_user u ON u.account = e.account + WHERE e.objectType = 'task' + AND e.`date` >= ? + AND e.`date` <= ? + """ + groupFilter + """ + + GROUP BY e.`date`, e.account, u.realname, e.objectID, t.name + ORDER BY e.`date` ASC, e.account ASC, e.objectID ASC + LIMIT 500 + """, params.toArray()); + + if (isQueryFailed(rows)) { + return "禅道用户组任务工时统计:查询失败," + rows.get(0).get("count"); + } + if (rows.isEmpty()) { + return "禅道用户组任务工时统计:无数据"; + } + + double totalConsumed = rows.stream() + .map(row -> row.get("consumed")) + .filter(Number.class::isInstance) + .map(Number.class::cast) + .mapToDouble(Number::doubleValue) + .sum(); + List> byUser = queryList(""" + SELECT e.account name, ROUND(SUM(e.consumed), 2) count + FROM zt_effort e + WHERE e.objectType = 'task' + AND e.`date` >= ? + AND e.`date` <= ? + """ + groupFilter + """ + + GROUP BY e.account + ORDER BY count DESC + """, params.toArray()); + String details = rows.stream() + .map(row -> "- 日期=%s | 账号=%s | 姓名=%s | 任务=%s %s | 消耗工时=%s".formatted( + row.getOrDefault("effortDate", "-"), + row.getOrDefault("account", "-"), + row.getOrDefault("realname", "-"), + row.getOrDefault("taskId", "-"), + row.getOrDefault("taskName", "-"), + row.getOrDefault("consumed", "-"))) + .collect(Collectors.joining("\n")); + + return """ + 禅道 %s 组任务工时统计: + - 时间范围:%s 至 %s + - 记录数:%s + - 总消耗工时:%.2f + - 按用户:%s + 明细: + %s + """.formatted(actualGroupName, start, end, rows.size(), totalConsumed, formatRows(byUser), details); + } + + @Tool(description = "通用禅道统计查询,支持 effort/task/story 三类数据,按白名单日期字段和分组维度统计;用户过滤支持账号或姓名;不接收 SQL") + public String queryZentaoStats( + @ToolParam(description = "统计对象:effort=工时,task=任务,story=需求") String entity, + @ToolParam(description = "开始日期,格式 yyyy-MM-dd") String startDate, + @ToolParam(description = "结束日期,格式 yyyy-MM-dd") String endDate, + @ToolParam(description = "日期字段:effort 支持 date;task 支持 assignedDate、finishedDate、closedDate、openedDate;story 支持 assignedDate、closedDate、openedDate", required = false) String dateField, + @ToolParam(description = "分组维度,多个用英文逗号分隔;effort 支持 date、account、task;task 支持 date、assignedTo、finishedBy、status;story 支持 date、assignedTo、status、stage", required = false) String groupBy, + @ToolParam(description = "禅道用户账号或姓名,支持单个或英文逗号分隔多个;为空表示不过滤用户", required = false) String accounts, + @ToolParam(description = "禅道用户组名称,例如 TSO;为空表示不过滤用户组", required = false) String groupName, + @ToolParam(description = "返回明细条数上限,默认 100,最大 500", required = false) Integer limit) { + LocalDate start = parseDate(startDate); + LocalDate end = parseDate(endDate); + if (start == null || end == null) { + return "禅道通用统计:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01"; + } + if (start.isAfter(end)) { + return "禅道通用统计:开始日期不能晚于结束日期"; + } + + String actualEntity = StringUtils.hasText(entity) ? entity.trim().toLowerCase() : ""; + return switch (actualEntity) { + case "effort" -> queryEffortStats(start, end, groupBy, accounts, groupName, limit); + case "task" -> queryTaskStats(start, end, dateField, groupBy, accounts, groupName, limit); + case "story" -> queryStoryStats(start, end, dateField, groupBy, accounts, groupName, limit); + default -> "禅道通用统计:entity 仅支持 effort、task、story"; + }; + } + + @Tool(description = "按完成开始日期和结束日期统计禅道任务完成情况,支持指定单个或多个用户账号/姓名,返回完成任务数、负责人分布和明细") + public String getZentaoTaskCompletionStats( + @ToolParam(description = "完成开始日期,格式 yyyy-MM-dd;本周请使用系统上下文给出的本周开始日期") String startDate, + @ToolParam(description = "完成结束日期,格式 yyyy-MM-dd;本周请使用系统上下文给出的本周结束日期") String endDate, + @ToolParam(description = "禅道用户账号或姓名,支持单个或多个,多个用英文逗号分隔;为空表示全部用户", required = false) String accounts) { + LocalDate start = parseDate(startDate); + LocalDate end = parseDate(endDate); + if (start == null || end == null) { + return "禅道任务完成情况:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01"; + } + if (start.isAfter(end)) { + return "禅道任务完成情况:开始日期不能晚于结束日期"; + } + + List accountList = parseAccounts(accounts); + List params = new ArrayList<>(); + params.add(start.toString()); + params.add(end.toString()); + + String accountFilter = ""; + if (!accountList.isEmpty()) { + accountFilter = buildUserIdentityFilter("COALESCE(NULLIF(t.finishedBy, ''), t.assignedTo)", accountList, params); + } + + String where = """ + t.deleted = '0' + AND t.finishedDate IS NOT NULL + AND t.finishedDate <> '0000-00-00 00:00:00' + AND t.finishedDate >= ? + AND t.finishedDate < ? + """ + accountFilter; + + Long total = queryLong("SELECT COUNT(1) FROM zt_task t WHERE " + where, params.toArray()); + List> byFinishedBy = queryList(""" + SELECT COALESCE(NULLIF(t.finishedBy, ''), t.assignedTo) name, COUNT(1) count + FROM zt_task t + WHERE """ + " " + where + """ + + GROUP BY COALESCE(NULLIF(t.finishedBy, ''), t.assignedTo) + ORDER BY count DESC + LIMIT 20 + """, params.toArray()); + List> byStatus = queryList(""" + SELECT t.status name, COUNT(1) count + FROM zt_task t + WHERE """ + " " + where + """ + + GROUP BY t.status + ORDER BY count DESC + """, params.toArray()); + List> details = queryList(""" + SELECT DATE(t.finishedDate) finishedDate, + COALESCE(NULLIF(t.finishedBy, ''), t.assignedTo) account, + COALESCE(u.realname, NULLIF(t.finishedBy, ''), t.assignedTo) realname, + t.id taskId, + t.name taskName, + t.status status, + t.consumed consumed + FROM zt_task t + LEFT JOIN zt_user u ON u.account = COALESCE(NULLIF(t.finishedBy, ''), t.assignedTo) + WHERE """ + " " + where + """ + + ORDER BY t.finishedDate ASC, account ASC, t.id ASC + LIMIT 500 + """, params.toArray()); + + if (isQueryFailed(details)) { + return "禅道任务完成情况:查询失败," + details.get(0).get("count"); + } + if (details.isEmpty()) { + return "禅道任务完成情况:无数据"; + } + + String rows = details.stream() + .map(row -> "- 完成日期=%s | 账号=%s | 姓名=%s | 任务=%s %s | 状态=%s | 消耗工时=%s".formatted( + row.getOrDefault("finishedDate", "-"), + row.getOrDefault("account", "-"), + row.getOrDefault("realname", "-"), + row.getOrDefault("taskId", "-"), + row.getOrDefault("taskName", "-"), + row.getOrDefault("status", "-"), + row.getOrDefault("consumed", "-"))) + .collect(Collectors.joining("\n")); + + return """ + 禅道任务完成情况: + - 完成时间范围:%s 至 %s + - 用户范围:%s + - 完成任务数:%s + - 按状态:%s + - 完成人 TOP20:%s + 明细: + %s + """.formatted(start, end, accountList.isEmpty() ? "全部用户" : String.join(",", accountList), + value(total), formatRows(byStatus), formatRows(byFinishedBy), rows); + } + + @Tool(description = "按指派开始日期和结束日期统计禅道需求完成情况,支持指定单个或多个用户账号/姓名,返回需求状态、阶段、完成率和明细") + public String getZentaoStoryCompletionStats( + @ToolParam(description = "指派开始日期,格式 yyyy-MM-dd,按需求 assignedDate 过滤") String startDate, + @ToolParam(description = "指派结束日期,格式 yyyy-MM-dd,按需求 assignedDate 过滤") String endDate, + @ToolParam(description = "禅道用户账号或姓名,支持单个或多个,多个用英文逗号分隔;为空表示全部用户", required = false) String accounts) { + LocalDate start = parseDate(startDate); + LocalDate end = parseDate(endDate); + if (start == null || end == null) { + return "禅道需求完成情况:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01"; + } + if (start.isAfter(end)) { + return "禅道需求完成情况:开始日期不能晚于结束日期"; + } + + List accountList = parseAccounts(accounts); + List params = new ArrayList<>(); + params.add(start.toString()); + params.add(end.plusDays(1).toString()); + + String accountFilter = ""; + if (!accountList.isEmpty()) { + accountFilter = buildUserIdentityFilter("s.assignedTo", accountList, params); + } + + String where = """ + s.deleted = '0' + AND s.assignedDate IS NOT NULL + AND s.assignedDate <> '0000-00-00 00:00:00' + AND s.assignedDate >= ? + AND s.assignedDate < ? + """ + accountFilter; + + List> summary = queryList(""" + SELECT COUNT(1) total, + SUM(CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END) completed, + ROUND(SUM(CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END) * 100 / NULLIF(COUNT(1), 0), 2) completionRate + FROM zt_story s + WHERE """ + " " + where, params.toArray()); + if (isQueryFailed(summary)) { + return "禅道需求完成情况:查询失败," + summary.get(0).get("count"); + } + + List> byStatus = queryList(""" + SELECT s.status name, COUNT(1) count + FROM zt_story s + WHERE """ + " " + where + """ + + GROUP BY s.status + ORDER BY count DESC + """, params.toArray()); + List> byStage = queryList(""" + SELECT s.stage name, COUNT(1) count + FROM zt_story s + WHERE """ + " " + where + """ + + GROUP BY s.stage + ORDER BY count DESC + """, params.toArray()); + List> details = queryList(""" + SELECT DATE(s.assignedDate) assignedDate, + s.assignedTo account, + COALESCE(u.realname, s.assignedTo) realname, + s.id storyId, + s.title title, + s.status status, + s.stage stage, + s.closedDate closedDate + FROM zt_story s + LEFT JOIN zt_user u ON u.account = s.assignedTo + WHERE """ + " " + where + """ + + ORDER BY s.assignedDate ASC, s.assignedTo ASC, s.id ASC + LIMIT 500 + """, params.toArray()); + if (isQueryFailed(details)) { + return "禅道需求完成情况:查询失败," + details.get(0).get("count"); + } + if (details.isEmpty()) { + return "禅道需求完成情况:无数据"; + } + + Map firstSummary = summary.isEmpty() ? Map.of() : summary.get(0); + String rows = details.stream() + .map(row -> "- 指派日期=%s | 账号=%s | 姓名=%s | 需求=%s %s | 状态=%s | 阶段=%s | 关闭时间=%s".formatted( + row.getOrDefault("assignedDate", "-"), + row.getOrDefault("account", "-"), + row.getOrDefault("realname", "-"), + row.getOrDefault("storyId", "-"), + row.getOrDefault("title", "-"), + row.getOrDefault("status", "-"), + row.getOrDefault("stage", "-"), + row.getOrDefault("closedDate", "-"))) + .collect(Collectors.joining("\n")); + + return """ + 禅道需求完成情况: + - 指派时间范围:%s 至 %s + - 用户范围:%s + - 需求总数:%s + - 已完成数:%s + - 完成率:%s%% + - 按状态:%s + - 按阶段:%s + 明细: + %s + """.formatted(start, end, accountList.isEmpty() ? "全部用户" : String.join(",", accountList), + firstSummary.getOrDefault("total", 0), + firstSummary.getOrDefault("completed", 0), + firstSummary.getOrDefault("completionRate", 0), + formatRows(byStatus), + formatRows(byStage), + rows); + } + + private SqlFilter resolveProductFilter(String product) { + if (!StringUtils.hasText(product)) { + return SqlFilter.empty(); + } + String value = product.trim(); + if (value.matches("\\d+")) { + return new SqlFilter(" AND b.product = ?", List.of(Long.valueOf(value))); + } + return new SqlFilter( + " AND b.product IN (SELECT p.id FROM zt_product p WHERE p.deleted = '0' AND p.name LIKE ?)", + List.of(likePattern(value))); + } + + private SqlFilter resolveProjectFilter(String project) { + if (!StringUtils.hasText(project)) { + return SqlFilter.empty(); + } + String value = project.trim(); + if (value.matches("\\d+")) { + return new SqlFilter(" AND t.project = ?", List.of(Long.valueOf(value))); + } + return new SqlFilter( + " AND t.project IN (SELECT p.id FROM zt_project p WHERE p.deleted = '0' AND p.name LIKE ?)", + List.of(likePattern(value))); + } + + private String queryEffortStats( + LocalDate start, LocalDate end, String groupBy, String accounts, String groupName, Integer limit) { + List dimensions = resolveDimensions(groupBy, List.of("date", "account", "task"), List.of("date", "account")); + String selectDimensions = dimensions.stream() + .map(this::effortDimensionSql) + .collect(Collectors.joining(", ")); + String groupDimensions = dimensions.stream() + .map(this::effortGroupSql) + .collect(Collectors.joining(", ")); + + List params = new ArrayList<>(); + params.add(start.toString()); + params.add(end.plusDays(1).toString()); + String userFilter = buildUserFilter("e.account", accounts, groupName, params); + int actualLimit = normalizeLimit(limit); + + List> rows = queryList(""" + SELECT %s, + COUNT(1) records, + ROUND(SUM(e.consumed), 2) consumed + FROM zt_effort e + LEFT JOIN zt_task t ON t.id = e.objectID AND e.objectType = 'task' + LEFT JOIN zt_user u ON u.account = e.account + WHERE e.objectType = 'task' + AND e.`date` >= ? + AND e.`date` <= ? + %s + GROUP BY %s + ORDER BY consumed DESC + LIMIT %s + """.formatted(selectDimensions, userFilter, groupDimensions, actualLimit), params.toArray()); + if (isQueryFailed(rows)) { + return "禅道工时统计:查询失败," + rows.get(0).get("count"); + } + return formatGenericStats("禅道工时统计", start, end, rows); + } + + private String queryTaskStats( + LocalDate start, LocalDate end, String dateField, String groupBy, + String accounts, String groupName, Integer limit) { + String actualDateField = taskDateField(dateField); + List dimensions = resolveDimensions( + groupBy, List.of("date", "assignedTo", "finishedBy", "status"), List.of("date", "status")); + String selectDimensions = dimensions.stream() + .map(dimension -> taskDimensionSql(dimension, actualDateField)) + .collect(Collectors.joining(", ")); + String groupDimensions = dimensions.stream() + .map(dimension -> taskGroupSql(dimension, actualDateField)) + .collect(Collectors.joining(", ")); + + List params = new ArrayList<>(); + params.add(start.toString()); + params.add(end.toString()); + String userFilter = buildUserFilter(resolveTaskUserColumn(dimensions), accounts, groupName, params); + int actualLimit = normalizeLimit(limit); + + List> rows = queryList(""" + SELECT %s, + COUNT(1) count, + ROUND(SUM(t.consumed), 2) consumed + FROM zt_task t + WHERE t.deleted = '0' + AND t.%s IS NOT NULL + AND t.%s <> '0000-00-00 00:00:00' + AND t.%s >= ? + AND t.%s < ? + %s + GROUP BY %s + ORDER BY count DESC + LIMIT %s + """.formatted(selectDimensions, actualDateField, actualDateField, actualDateField, + actualDateField, userFilter, groupDimensions, actualLimit), params.toArray()); + if (isQueryFailed(rows)) { + return "禅道任务统计:查询失败," + rows.get(0).get("count"); + } + return formatGenericStats("禅道任务统计(日期字段:" + actualDateField + ")", start, end, rows); + } + + private String queryStoryStats( + LocalDate start, LocalDate end, String dateField, String groupBy, + String accounts, String groupName, Integer limit) { + String actualDateField = storyDateField(dateField); + List dimensions = resolveDimensions( + groupBy, List.of("date", "assignedTo", "status", "stage"), List.of("date", "status")); + String selectDimensions = dimensions.stream() + .map(dimension -> storyDimensionSql(dimension, actualDateField)) + .collect(Collectors.joining(", ")); + String groupDimensions = dimensions.stream() + .map(dimension -> storyGroupSql(dimension, actualDateField)) + .collect(Collectors.joining(", ")); + + List params = new ArrayList<>(); + params.add(start.toString()); + params.add(end.plusDays(1).toString()); + String userFilter = buildUserFilter("s.assignedTo", accounts, groupName, params); + int actualLimit = normalizeLimit(limit); + + List> rows = queryList(""" + SELECT %s, + COUNT(1) count, + SUM(CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END) completed + FROM zt_story s + WHERE s.deleted = '0' + AND s.%s IS NOT NULL + AND s.%s <> '0000-00-00 00:00:00' + AND s.%s >= ? + AND s.%s < ? + %s + GROUP BY %s + ORDER BY count DESC + LIMIT %s + """.formatted(selectDimensions, actualDateField, actualDateField, actualDateField, + actualDateField, userFilter, groupDimensions, actualLimit), params.toArray()); + if (isQueryFailed(rows)) { + return "禅道需求统计:查询失败," + rows.get(0).get("count"); + } + return formatGenericStats("禅道需求统计(日期字段:" + actualDateField + ")", start, end, rows); + } + + private String buildUserFilter(String accountColumn, String accounts, String groupName, List params) { + List accountList = parseAccounts(accounts); + StringBuilder filter = new StringBuilder(); + if (!accountList.isEmpty()) { + filter.append(buildUserIdentityFilter(accountColumn, accountList, params)); + } + if (StringUtils.hasText(groupName)) { + filter.append(""" + AND %s IN ( + SELECT ug.account + FROM zt_usergroup ug + INNER JOIN zt_group g ON g.id = ug.`group` + INNER JOIN zt_user u2 ON u2.account = ug.account + WHERE g.`name` = ? AND u2.deleted = '0' + ) + """.formatted(accountColumn)); + params.add(groupName.trim()); + } + return filter.toString(); + } + + private String buildUserIdentityFilter(String accountColumn, List values, List params) { + if (values == null || values.isEmpty()) { + return ""; + } + StringBuilder filter = new StringBuilder() + .append(" AND EXISTS (") + .append(" SELECT 1 FROM zt_user uf") + .append(" WHERE uf.deleted = '0'") + .append(" AND uf.account = ").append(accountColumn) + .append(" AND (uf.account IN (").append(placeholders(values.size())).append(")") + .append(" OR uf.realname IN (").append(placeholders(values.size())).append(")"); + params.addAll(values); + params.addAll(values); + for (String value : values) { + filter.append(" OR uf.realname LIKE ?"); + params.add(likePattern(value)); + } + filter.append("))\n"); + return filter.toString(); + } + + private List resolveDimensions(String groupBy, List allowed, List defaults) { + if (!StringUtils.hasText(groupBy)) { + return defaults; + } + List dimensions = List.of(groupBy.split(",")).stream() + .map(String::trim) + .filter(StringUtils::hasText) + .filter(allowed::contains) + .distinct() + .toList(); + return dimensions.isEmpty() ? defaults : dimensions; + } + + private String effortDimensionSql(String dimension) { + return switch (dimension) { + case "date" -> "e.`date` date"; + case "account" -> "e.account account"; + case "task" -> "e.objectID taskId"; + default -> throw new IllegalArgumentException("unsupported effort dimension: " + dimension); + }; + } + + private String effortGroupSql(String dimension) { + return switch (dimension) { + case "date" -> "e.`date`"; + case "account" -> "e.account"; + case "task" -> "e.objectID"; + default -> throw new IllegalArgumentException("unsupported effort dimension: " + dimension); + }; + } + + private String taskDimensionSql(String dimension, String dateField) { + return switch (dimension) { + case "date" -> "DATE(t." + dateField + ") date"; + case "assignedTo" -> "t.assignedTo assignedTo"; + case "finishedBy" -> "t.finishedBy finishedBy"; + case "status" -> "t.status status"; + default -> throw new IllegalArgumentException("unsupported task dimension: " + dimension); + }; + } + + private String taskGroupSql(String dimension, String dateField) { + return switch (dimension) { + case "date" -> "DATE(t." + dateField + ")"; + case "assignedTo" -> "t.assignedTo"; + case "finishedBy" -> "t.finishedBy"; + case "status" -> "t.status"; + default -> throw new IllegalArgumentException("unsupported task dimension: " + dimension); + }; + } + + private String storyDimensionSql(String dimension, String dateField) { + return switch (dimension) { + case "date" -> "DATE(s." + dateField + ") date"; + case "assignedTo" -> "s.assignedTo assignedTo"; + case "status" -> "s.status status"; + case "stage" -> "s.stage stage"; + default -> throw new IllegalArgumentException("unsupported story dimension: " + dimension); + }; + } + + private String storyGroupSql(String dimension, String dateField) { + return switch (dimension) { + case "date" -> "DATE(s." + dateField + ")"; + case "assignedTo" -> "s.assignedTo"; + case "status" -> "s.status"; + case "stage" -> "s.stage"; + default -> throw new IllegalArgumentException("unsupported story dimension: " + dimension); + }; + } + + private String taskDateField(String dateField) { + if (!StringUtils.hasText(dateField)) { + return "finishedDate"; + } + return switch (dateField.trim()) { + case "assignedDate", "finishedDate", "closedDate", "openedDate" -> dateField.trim(); + default -> "finishedDate"; + }; + } + + private String storyDateField(String dateField) { + if (!StringUtils.hasText(dateField)) { + return "assignedDate"; + } + return switch (dateField.trim()) { + case "assignedDate", "closedDate", "openedDate" -> dateField.trim(); + default -> "assignedDate"; + }; + } + + private String resolveTaskUserColumn(List dimensions) { + return dimensions.contains("finishedBy") ? "t.finishedBy" : "t.assignedTo"; + } + + private int normalizeLimit(Integer limit) { + if (limit == null || limit <= 0) { + return 100; + } + return Math.min(limit, 500); + } + + private String formatGenericStats(String title, LocalDate start, LocalDate end, List> rows) { + if (rows == null || rows.isEmpty()) { + return title + ":无数据"; + } + String details = rows.stream() + .map(row -> "- " + row.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining(" | "))) + .collect(Collectors.joining("\n")); + return """ + %s: + - 时间范围:%s 至 %s + - 返回行数:%s + 明细: + %s + """.formatted(title, start, end, rows.size(), details); + } + + private Long queryLong(String sql) { + try { + logSql(sql); + return zentaoJdbcTemplate.queryForObject(sql, Long.class); + } catch (Exception ex) { + log.warn("[Agent][Zentao][SQL] queryLong failed, sql={}", sql, ex); + return null; + } + } + + private Long queryLong(String sql, Object... args) { + try { + logSql(sql, args); + return zentaoJdbcTemplate.queryForObject(sql, Long.class, args); + } catch (Exception ex) { + log.warn("[Agent][Zentao][SQL] queryLong failed, sql={}, args={}", sql, args, ex); + return null; + } + } + + private List> queryList(String sql) { + try { + logSql(sql); + return zentaoJdbcTemplate.queryForList(sql); + } catch (Exception ex) { + log.warn("[Agent][Zentao][SQL] queryList failed, sql={}", sql, ex); + return List.of(Map.of("name", "查询失败", "count", ex.getMessage())); + } + } + + private List> queryList(String sql, Object... args) { + try { + logSql(sql, args); + return zentaoJdbcTemplate.queryForList(sql, args); + } catch (Exception ex) { + log.warn("[Agent][Zentao][SQL] queryList failed, sql={}, args={}", sql, args, ex); + return List.of(Map.of("name", "查询失败", "count", ex.getMessage())); + } + } + + private void logSql(String sql, Object... args) { + if (args == null || args.length == 0) { + log.info("[Agent][Zentao][SQL] {}", normalizeSql(sql)); + return; + } + log.info("[Agent][Zentao][SQL] {} | args={} | expanded={}", + normalizeSql(sql), List.of(args), expandSql(sql, args)); + } + + private String normalizeSql(String sql) { + return sql == null ? "" : sql.replaceAll("\\s+", " ").trim(); + } + + private String expandSql(String sql, Object... args) { + String normalized = normalizeSql(sql); + if (args == null || args.length == 0) { + return normalized; + } + StringBuilder builder = new StringBuilder(); + int argIndex = 0; + for (int i = 0; i < normalized.length(); i++) { + char current = normalized.charAt(i); + if (current == '?' && argIndex < args.length) { + builder.append(formatSqlParam(args[argIndex++])); + } else { + builder.append(current); + } + } + return builder.toString(); + } + + private String formatSqlParam(Object value) { + if (value == null) { + return "NULL"; + } + if (value instanceof Number || value instanceof Boolean) { + return value.toString(); + } + return "'" + value.toString().replace("'", "''") + "'"; + } + + private LocalDate parseDate(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + try { + return LocalDate.parse(value.trim()); + } catch (DateTimeParseException ex) { + return null; + } + } + + private List parseAccounts(String accounts) { + if (!StringUtils.hasText(accounts)) { + return List.of(); + } + return List.of(accounts.split(",")).stream() + .map(String::trim) + .filter(StringUtils::hasText) + .distinct() + .toList(); + } + + private String placeholders(int size) { + return java.util.stream.IntStream.range(0, size) + .mapToObj(index -> "?") + .collect(Collectors.joining(",")); + } + + private boolean isQueryFailed(List> rows) { + return rows != null + && rows.size() == 1 + && "查询失败".equals(rows.get(0).get("name")) + && rows.get(0).containsKey("count"); + } + + private String likePattern(String value) { + String escaped = value.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_"); + return "%" + escaped + "%"; + } + + private String formatRows(List> rows) { + if (rows == null || rows.isEmpty()) { + return "无数据"; + } + return rows.stream() + .map(row -> row.get("name") + "=" + row.get("count")) + .collect(Collectors.joining(",")); + } + + private String value(Long value) { + return value == null ? "查询失败" : String.valueOf(value); + } + + private String scopeText(String value) { + return StringUtils.hasText(value) ? "(范围:" + value.trim() + ")" : ""; + } + + private record SqlFilter(String clause, List params) { + + private static SqlFilter empty() { + return new SqlFilter("", List.of()); + } + } +} diff --git a/businessAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/businessAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..85b3122 --- /dev/null +++ b/businessAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.will.businessAgent.config.BusinessAgentAutoConfiguration diff --git a/businessJob/build.gradle b/businessJob/build.gradle new file mode 100644 index 0000000..ea88166 --- /dev/null +++ b/businessJob/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(':coreCommon') + implementation project(':coreService') + + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'com.fasterxml.jackson.core:jackson-databind' +} + +sourceSets { + main { + java { + srcDirs = ['src/main/java'] + } + } +} diff --git a/businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java b/businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java new file mode 100644 index 0000000..87a5d60 --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java @@ -0,0 +1,51 @@ +package com.will.businessJob.mail; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.mail.MailProperties; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MailMessageClient { + + private final ObjectProvider mailSenderProvider; + private final ObjectProvider mailPropertiesProvider; + + public void sendMarkdown(String mailTo, String subject, String content) { + if (!StringUtils.hasText(mailTo)) { + throw new IllegalStateException("邮件收件人未配置"); + } + JavaMailSender mailSender = mailSenderProvider.getIfAvailable(); + if (mailSender == null) { + throw new IllegalStateException("邮件发送服务未配置,请配置 spring.mail.*"); + } + try { + MimeMessage message = mailSender.createMimeMessage(); + MailProperties mailProperties = mailPropertiesProvider.getIfAvailable(MailProperties::new); + String from = defaultString(mailProperties.getProperties().get("mail.from"), mailProperties.getUsername()); + if (StringUtils.hasText(from)) { + message.setFrom(new InternetAddress(from.trim())); + } + message.setRecipients(MimeMessage.RecipientType.TO, mailTo.trim()); + message.setSubject(defaultString(subject, "计划任务通知"), "UTF-8"); + message.setText(content == null ? "" : content, "UTF-8"); + mailSender.send(message); + log.info("[MailMessage] markdown sent, mailTo={}, length={}", mailTo, content == null ? 0 : content.length()); + } catch (Exception ex) { + log.warn("[MailMessage] send failed, mailTo={}", mailTo, ex); + throw new IllegalStateException("邮件发送失败:" + ex.getMessage(), ex); + } + } + + private String defaultString(String value, String defaultValue) { + return StringUtils.hasText(value) ? value.trim() : defaultValue; + } +} \ No newline at end of file diff --git a/businessJob/src/main/java/com/will/businessJob/robot/RobotMessageClient.java b/businessJob/src/main/java/com/will/businessJob/robot/RobotMessageClient.java new file mode 100644 index 0000000..a22c23a --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/robot/RobotMessageClient.java @@ -0,0 +1,102 @@ +package com.will.businessJob.robot; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.will.coreService.enums.RobotType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RobotMessageClient { + + private static final String DEFAULT_TITLE = "报表通知"; + + private final ObjectMapper objectMapper; + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + public void sendMarkdown(String robotType, String content, String webhookUrl) { + RobotType type = RobotType.valueOf(RobotType.defaultName(robotType)); + if (!StringUtils.hasText(webhookUrl)) { + throw new IllegalStateException(type.getLabel() + "机器人 webhookUrl 未配置"); + } + if (!StringUtils.hasText(content)) { + throw new IllegalArgumentException(type.getLabel() + "机器人消息内容不能为空"); + } + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(webhookUrl.trim())) + .timeout(Duration.ofSeconds(20)) + .header("Content-Type", "application/json; charset=utf-8") + .POST(HttpRequest.BodyPublishers.ofString( + objectMapper.writeValueAsString(buildPayload(type, content)), StandardCharsets.UTF_8)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IllegalStateException(type.getLabel() + "机器人 HTTP 状态异常:" + response.statusCode()); + } + validateResponse(type, response.body()); + log.info("[RobotMessage] markdown sent, robotType={}, length={}", type.name(), content.length()); + } catch (Exception ex) { + log.warn("[RobotMessage] send failed, robotType={}", type.name(), ex); + throw new IllegalStateException(type.getLabel() + "机器人发送失败:" + ex.getMessage(), ex); + } + } + + private Map buildPayload(RobotType type, String content) { + return switch (type) { + case WE_COM -> Map.of( + "msgtype", "markdown", + "markdown", Map.of("content", content) + ); + case FEISHU -> Map.of( + "msg_type", "interactive", + "card", Map.of( + "elements", java.util.List.of(Map.of( + "tag", "markdown", + "content", content + )) + ) + ); + case DINGTALK -> Map.of( + "msgtype", "markdown", + "markdown", Map.of( + "title", DEFAULT_TITLE, + "text", content + ) + ); + case EMAIL -> throw new IllegalArgumentException("EMAIL is not a robot message type"); + }; + } + + private void validateResponse(RobotType type, String responseBody) throws Exception { + JsonNode body = objectMapper.readTree(responseBody); + switch (type) { + case WE_COM, DINGTALK -> { + int errcode = body.path("errcode").asInt(-1); + if (errcode != 0) { + throw new IllegalStateException(type.getLabel() + "机器人返回失败:" + responseBody); + } + } + case FEISHU -> { + int code = body.path("code").asInt(body.path("StatusCode").asInt(-1)); + if (code != 0) { + throw new IllegalStateException(type.getLabel() + "机器人返回失败:" + responseBody); + } + } + case EMAIL -> throw new IllegalArgumentException("EMAIL is not a robot message type"); + } + } +} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java new file mode 100644 index 0000000..3d8c2f8 --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java @@ -0,0 +1,144 @@ +package com.will.businessJob.zentao; + +import com.zaxxer.hikari.HikariDataSource; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@ConditionalOnBean(name = "zentaoJdbcTemplate") +public class ZentaoEffortReportQueryService { + + private final JdbcTemplate zentaoJdbcTemplate; + + public ZentaoEffortReportQueryService(@Qualifier("zentaoJdbcTemplate") JdbcTemplate zentaoJdbcTemplate) { + this.zentaoJdbcTemplate = zentaoJdbcTemplate; + } + + @PostConstruct + public void logZentaoJdbcTemplate() { + Object dataSource = zentaoJdbcTemplate.getDataSource(); + if (dataSource instanceof HikariDataSource hikariDataSource) { + log.info("[Job][Zentao] effort report bound jdbcUrl={}, username={}", + hikariDataSource.getJdbcUrl(), hikariDataSource.getUsername()); + } else { + log.info("[Job][Zentao] effort report bound datasource={}", + dataSource == null ? "null" : dataSource.getClass().getName()); + } + } + + public String getGroupTaskEffortRobotSummary(String startDate, String endDate, String groupName) { + LocalDate start = parseDate(startDate); + LocalDate end = parseDate(endDate); + if (start == null || end == null) { + return "禅道用户组任务工时统计:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01"; + } + if (start.isAfter(end)) { + return "禅道用户组任务工时统计:开始日期不能晚于结束日期"; + } + String actualGroupName = StringUtils.hasText(groupName) ? groupName.trim() : "TSO"; + List params = List.of(start.toString(), end.toString(), actualGroupName); + + List> rows = queryList(""" + SELECT u.account account, + COALESCE(NULLIF(u.realname, ''), u.account) realname, + COALESCE(effort.consumed, 0) consumed + FROM zt_user u + INNER JOIN zt_usergroup ug ON ug.account = u.account + INNER JOIN zt_group g ON g.id = ug.`group` + LEFT JOIN ( + SELECT e.account, ROUND(SUM(e.consumed), 2) consumed + FROM zt_effort e + WHERE e.objectType = 'task' + AND e.`date` >= ? + AND e.`date` <= ? + GROUP BY e.account + ) effort ON effort.account = u.account + WHERE g.`name` = ? + AND u.deleted = '0' + AND u.account <> 'sygu' + ORDER BY consumed ASC, u.account ASC + """, params.toArray()); + + if (isQueryFailed(rows)) { + return "禅道用户组任务工时统计:查询失败," + rows.get(0).get("count"); + } + if (rows.isEmpty()) { + return "禅道用户组任务工时统计:无成员数据"; + } + + long days = end.toEpochDay() - start.toEpochDay() + 1; + double threshold = Math.max(days - 1, 0) * 7D; + double totalConsumed = rows.stream() + .map(row -> row.get("consumed")) + .filter(Number.class::isInstance) + .map(Number.class::cast) + .mapToDouble(Number::doubleValue) + .sum(); + String users = rows.stream() + .map(row -> { + Object realname = row.getOrDefault("realname", "-"); + Object consumedValue = row.getOrDefault("consumed", 0); + double consumed = consumedValue instanceof Number number ? number.doubleValue() : 0D; + String line = "姓名=%s | 消耗工时=%s".formatted(realname, consumedValue); + String color = consumed < threshold ? "warning" : "info"; + return "- " + line + ""; + }) + .collect(Collectors.joining("\n")); + + return """ + 禅道 %s 组任务工时统计: + - 时间范围:%s 至 %s + - 人员数:%s + - 工时预警阈值:%.2f + - 总消耗工时:%.2f + 按成员: + %s + """.formatted(actualGroupName, start, end, rows.size(), threshold, totalConsumed, users); + } + + private LocalDate parseDate(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + try { + return LocalDate.parse(value.trim()); + } catch (DateTimeParseException ex) { + return null; + } + } + + private List> queryList(String sql, Object... args) { + try { + logSql(sql, args); + return zentaoJdbcTemplate.queryForList(sql, args); + } catch (Exception ex) { + log.warn("[Job][Zentao][SQL] query failed: {}", ex.getMessage(), ex); + return List.of(Map.of("count", ex.getMessage() == null ? "bad SQL grammar" : ex.getMessage())); + } + } + + private boolean isQueryFailed(List> rows) { + return rows.size() == 1 && rows.get(0).containsKey("count") && rows.get(0).size() == 1 + && rows.get(0).get("count") instanceof String; + } + + private void logSql(String sql, Object... args) { + log.info("[Job][Zentao][SQL] {} | args={}", normalizeSql(sql), List.of(args)); + } + + private String normalizeSql(String sql) { + return sql == null ? "" : sql.replaceAll("\\s+", " ").trim(); + } +} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeComReportService.java b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeComReportService.java new file mode 100644 index 0000000..178f896 --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeComReportService.java @@ -0,0 +1,163 @@ +package com.will.businessJob.zentao; + +import com.will.businessJob.mail.MailMessageClient; +import com.will.businessJob.robot.RobotMessageClient; +import com.will.coreService.dto.AgentReportConfigDTO; +import com.will.coreService.dto.AgentReportJobConfigDTO; +import com.will.coreService.enums.RobotType; +import com.will.coreService.service.AgentReportConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ZentaoWeComReportService { + + private static final String DEFAULT_GROUP_NAME = "TSO"; + private static final String DEFAULT_REPORT_TITLE = "禅道工时完成情况"; + private static final String DEFAULT_ZONE = "Asia/Shanghai"; + private static final int WE_COM_MARKDOWN_MAX_LENGTH = 3900; + + private final ObjectProvider effortReportQueryService; + private final RobotMessageClient robotMessageClient; + private final MailMessageClient mailMessageClient; + private final AgentReportConfigService reportConfigService; + + public String sendDailyEffortReport(AgentReportJobConfigDTO jobConfig) { + AgentReportConfigDTO config = reportConfigService.getConfig(); + String disabledReason = validateJobConfig(config, jobConfig); + if (disabledReason != null) { + return disabledReason; + } + + ReportRequest request = buildRequest(config, jobConfig); + return sendReport(request, jobConfig.getId()); + } + + private String validateJobConfig(AgentReportConfigDTO config, AgentReportJobConfigDTO jobConfig) { + if (!Boolean.TRUE.equals(config.getEnabled()) || !Boolean.TRUE.equals(jobConfig.getEnabled())) { + return "计划任务未启用"; + } + return null; + } + + private ReportRequest buildRequest(AgentReportConfigDTO config, AgentReportJobConfigDTO jobConfig) { + ZoneId zoneId = resolveZone(jobConfig.getZone()); + LocalDate today = LocalDate.now(zoneId); + LocalDate weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + return new ReportRequest( + defaultString(jobConfig.getName(), DEFAULT_REPORT_TITLE), + weekStart, + today, + defaultGroupName(config.getGroupName()), + defaultString(jobConfig.getRobotType(), RobotType.defaultName(config.getRobotType())), + defaultString(jobConfig.getWebhookUrl(), config.getWebhookUrl()), + defaultString(jobConfig.getMailTo(), config.getMailTo()), + defaultString(jobConfig.getMailSubject(), defaultString(config.getMailSubject(), defaultString(jobConfig.getName(), DEFAULT_REPORT_TITLE))) + ); + } + + private String sendReport(ReportRequest request, String jobId) { + ZentaoEffortReportQueryService queryService = getEffortReportQueryService(); + String report = queryService.getGroupTaskEffortRobotSummary( + request.startDate().toString(), + request.endDate().toString(), + request.groupName() + ); + String content = truncateMarkdown(request.robotType(), buildMarkdown(request, report)); + if (RobotType.EMAIL.name().equals(RobotType.defaultName(request.robotType()))) { + mailMessageClient.sendMarkdown(request.mailTo(), request.mailSubject(), content); + } else { + robotMessageClient.sendMarkdown(request.robotType(), content, request.webhookUrl()); + } + + if (StringUtils.hasText(jobId)) { + log.info("[Zentao][Robot] job report sent, jobId={}, robotType={}, startDate={}, endDate={}, groupName={}", + jobId, request.robotType(), request.startDate(), request.endDate(), request.groupName()); + } else { + log.info("[Zentao][Robot] daily effort report sent, robotType={}, startDate={}, endDate={}, groupName={}", + request.robotType(), request.startDate(), request.endDate(), request.groupName()); + } + return content; + } + + private ZentaoEffortReportQueryService getEffortReportQueryService() { + ZentaoEffortReportQueryService queryService = effortReportQueryService.getIfAvailable(); + if (queryService == null) { + throw new IllegalStateException("禅道工时报表通知查询服务未启用"); + } + return queryService; + } + + private String buildMarkdown(ReportRequest request, String report) { + return """ + ## %s + > 日期:%s - %s + > 执行范围:%s + + %s + """.formatted( + request.title(), + request.startDate(), + request.endDate(), + request.groupName(), + normalizeMarkdown(report) + ); + } + + private ZoneId resolveZone(String zone) { + return ZoneId.of(defaultString(zone, DEFAULT_ZONE)); + } + + private String defaultGroupName(String groupName) { + return defaultString(groupName, DEFAULT_GROUP_NAME); + } + + private String defaultString(String value, String defaultValue) { + return StringUtils.hasText(value) ? value.trim() : defaultValue; + } + + private String normalizeMarkdown(String report) { + if (!StringUtils.hasText(report)) { + return "无数据"; + } + return report.replace("\\n", "\n").trim(); + } + + private String truncateMarkdown(String robotType, String content) { + if (!RobotType.WE_COM.name().equals(RobotType.defaultName(robotType)) || content.length() <= WE_COM_MARKDOWN_MAX_LENGTH) { + return content; + } + String suffix = "\n\n> 内容过长,已截断。"; + int maxBodyLength = WE_COM_MARKDOWN_MAX_LENGTH - suffix.length(); + if (maxBodyLength <= 0) { + return content.substring(0, WE_COM_MARKDOWN_MAX_LENGTH); + } + int cutIndex = content.lastIndexOf('\n', maxBodyLength); + if (cutIndex < 0) { + cutIndex = maxBodyLength; + } + return content.substring(0, cutIndex).trim() + suffix; + } + + private record ReportRequest( + String title, + LocalDate startDate, + LocalDate endDate, + String groupName, + String robotType, + String webhookUrl, + String mailTo, + String mailSubject + ) { + } +} diff --git a/coreAgent/_mcp_test.gradle b/coreAgent/_mcp_test.gradle new file mode 100644 index 0000000..cca56b7 --- /dev/null +++ b/coreAgent/_mcp_test.gradle @@ -0,0 +1,3 @@ +plugins { id 'java' } +dependencyManagement { imports { mavenBom 'org.springframework.ai:spring-ai-bom:1.0.0' } } +dependencies { implementation 'org.springframework.ai:spring-ai-starter-mcp-client' } diff --git a/coreAgent/build.gradle b/coreAgent/build.gradle new file mode 100644 index 0000000..7436954 --- /dev/null +++ b/coreAgent/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(':coreCommon') + implementation project(':coreService') + + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.ai:spring-ai-client-chat' + implementation 'org.springframework.ai:spring-ai-ollama' + implementation 'org.springframework.ai:spring-ai-openai' + implementation 'org.springframework.ai:spring-ai-zhipuai' + implementation 'org.springframework.ai:spring-ai-starter-mcp-client' + + implementation 'org.bsc.langgraph4j:langgraph4j-core' + implementation 'org.bsc.langgraph4j:langgraph4j-spring-ai' +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/context/AgentOperatorContext.java b/coreAgent/src/main/java/com/will/coreAgent/agent/context/AgentOperatorContext.java new file mode 100644 index 0000000..b5a19af --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/context/AgentOperatorContext.java @@ -0,0 +1,32 @@ +package com.will.coreAgent.agent.context; + +import org.springframework.util.StringUtils; + +/** + * Agent 调用期间的操作人上下文(跨工具回调线程传递)。 + */ +public final class AgentOperatorContext { + + public static final String TOOL_CONTEXT_USER_ID = "operatorUserId"; + + private static final InheritableThreadLocal CURRENT_USER_ID = new InheritableThreadLocal<>(); + + private AgentOperatorContext() { + } + + public static void set(String userId) { + if (StringUtils.hasText(userId)) { + CURRENT_USER_ID.set(userId); + } else { + CURRENT_USER_ID.remove(); + } + } + + public static String get() { + return CURRENT_USER_ID.get(); + } + + public static void clear() { + CURRENT_USER_ID.remove(); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatRequest.java b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatRequest.java new file mode 100644 index 0000000..db67af1 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatRequest.java @@ -0,0 +1,13 @@ +package com.will.coreAgent.agent.dto; + +import lombok.Data; + +@Data +public class AgentChatRequest { + + /** 用户消息 */ + private String message; + + /** 会话 ID,留空则自动生成 */ + private String sessionId; +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatResult.java b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatResult.java new file mode 100644 index 0000000..2a24a89 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatResult.java @@ -0,0 +1,12 @@ +package com.will.coreAgent.agent.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class AgentChatResult { + + private String sessionId; + private String reply; +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentDefinition.java b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentDefinition.java new file mode 100644 index 0000000..4458579 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentDefinition.java @@ -0,0 +1,15 @@ +package com.will.coreAgent.agent.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class AgentDefinition { + + private String id; + private String name; + private String description; + private String endpoint; + private boolean streamingSupported; +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentMetaResult.java b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentMetaResult.java new file mode 100644 index 0000000..0791270 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentMetaResult.java @@ -0,0 +1,19 @@ +package com.will.coreAgent.agent.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class AgentMetaResult { + + private boolean enabled; + private String provider; + private String model; + private boolean mcpEnabled; + private int mcpToolCount; + private List mcpToolNames; + private List agents; +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/dto/McpStatusResult.java b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/McpStatusResult.java new file mode 100644 index 0000000..79f4337 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/dto/McpStatusResult.java @@ -0,0 +1,14 @@ +package com.will.coreAgent.agent.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class McpStatusResult { + private boolean enabled; + private int connectedServers; + private List toolNames; +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/prompt/WillSystemPrompts.java b/coreAgent/src/main/java/com/will/coreAgent/agent/prompt/WillSystemPrompts.java new file mode 100644 index 0000000..d077bd7 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/prompt/WillSystemPrompts.java @@ -0,0 +1,199 @@ +package com.will.coreAgent.agent.prompt; + + + +/** + + * willSystem 业务代码生成提示词(对齐现有分层:coreData / coreService / coreApi)。 + + */ + +public final class WillSystemPrompts { + + + + private WillSystemPrompts() { + + } + + + + public static final String PROJECT_CONVENTION = """ + + 你是 willSystem 后端代码生成助手。必须严格遵循: + + - 分层:Entity+Mapper(coreData) → DTO+Service(coreService) → Controller(coreApi) + + - Controller 路径:/api/sys/{module},返回 ResponseMessage / ResponseMessageFactory + + - 权限:@RequirePermission(ApiPermissions.{MODULE}_LIST) 等,权限码格式 sys:{module}:list|save|delete + + - Entity 继承 BaseEntity,@TableName/@TableId/@TableField,id 为 String + + - Mapper 继承 BaseMapper,位于 coreData.mapper 包 + + - Service 使用 @RequiredArgsConstructor + MyBatis-Plus IService + + - 不要引入 coreApi 对 coreData 实体的直接依赖 + + - 只输出 Java 代码,不要 markdown 代码块包裹 + + """; + + public static final String ASSISTANT_SYSTEM = """ + + 你是 willSystem 平台的 AI 智能助手,也是通用对话助手,熟悉以下技术栈: + + - 后端:Spring Boot 3、Java 21、Sa-Token、MyBatis-Plus、Gradle 多模块 + + - 模块:coreData / coreService / coreApi / coreAuth / coreAgent / wcsApp + + - 前端:Vue 3 + Vben Admin (willWeb) + + - AI:Spring AI + LangGraph4j + MCP 工具 + + - 当前模型信息以系统配置为准;如果用户询问“当前调用哪个模型”,不要按模型自称回答,不要编造 DeepSeek、GPT、GLM 等名称 + + 职责: + + 1. 解答 willSystem 架构、开发、部署问题 + + 2. 帮助设计业务模块(实体、API、权限、菜单) + + 3. 回答一般性问题(编程、技术概念等) + + 工具使用(重要): + + - 用户询问部门、组织架构时,必须调用 listDepartments / searchDepartments,不要编造 + + - 用户询问人员、账号时,必须调用 searchUsers / getUserDetail / listUsersByDepartment,不要编造 + + - 用户询问 willSystem 模块时,可调用 getWillSystemModuleInfo 工具 + + - 用户询问禅道统计报表时,优先调用 queryZentaoStats 进行结构化统计;只传 entity/dateField/groupBy/accounts/groupName/startDate/endDate 等参数,不要生成 SQL;accounts 参数既可以传禅道账号,也可以传用户姓名,多个用户用英文逗号分隔 + - 用户询问禅道、Bug、缺陷、任务、项目、日报/周报汇总时,必须调用 getZentaoUserSummary / getZentaoBugSummary / getZentaoTaskSummary / getZentaoDailyReport,不要编造 + - 用户询问禅道本周任务完成情况、按完成时间统计任务、指定账号/姓名或批量用户完成任务时,必须调用 getZentaoTaskCompletionStats;如果用户说“本周”,使用系统时间上下文中的本周范围,不要反问日期 + - 用户询问禅道任务工时、按时间统计工时、指定账号/姓名或批量用户工时时,必须调用 getZentaoTaskEffortStats,不要编造 + - 用户询问禅道需求完成情况、按指派时间查询需求、指定账号/姓名或批量用户需求完成率时,必须调用 getZentaoStoryCompletionStats,不要编造 + + - 用户询问禅道 TSO 用户组、TSO 成员、TSO 人员名单时,必须调用 listZentaoTsoUsers,不要编造 + + 回答要求:简洁准确,使用中文;列表、分点请换行输出(使用真正的换行,不要挤在一行)。 + + """; + + + + public static String planPrompt( + + String requirement, String moduleName, String entityName, String mcpContext) { + + return PROJECT_CONVENTION + """ + + + + %s + + + + 请为以下需求输出 JSON 生成计划(仅 JSON,无其它文字): + + { + + "moduleName": "%s", + + "entityName": "%s", + + "fields": [{"name":"字段名","javaType":"String","comment":"说明"}], + + "apis": ["list","save","delete"], + + "permissionPrefix": "sys:%s" + + } + + + + 业务需求:%s + + """.formatted(blankToNone(mcpContext), moduleName, entityName, moduleName, requirement); + + } + + + + public static String codePrompt( + + String layer, String moduleName, String entityName, String plan, String mcpContext) { + + return PROJECT_CONVENTION + """ + + + + %s + + + + 根据计划生成 %s 层代码。 + + moduleName=%s, entityName=%s + + 计划:%s + + + + 输出要求: + + - layer=entity:生成 coreData.entity.%s 实体类 + + - layer=mapper:生成 coreData.mapper.%sMapper 接口,继承 BaseMapper<%s> + + - layer=dto:生成 coreService.dto.%sDTO + + - layer=service:生成 coreService.service.%sService(含 page/save/removeBatchByIds) + + - layer=controller:生成 coreApi.controller.Auth%sController,路径 /api/sys/%s + + """.formatted( + + blankToNone(mcpContext), layer, moduleName, entityName, plan, + + entityName, entityName, entityName, entityName, entityName, entityName, moduleName); + + } + + + + public static String summaryPrompt( + + String moduleName, String entityName, String plan, String mcpContext) { + + return """ + + %s + + 用中文简要总结已为模块 %s(实体 %s)生成的代码结构及后续接入步骤(执行 SQL、菜单权限、前端页面),3-5 条 bullet。 + + 计划:%s + + """.formatted(blankToNone(mcpContext), moduleName, entityName, plan); + + } + + + + private static String blankToNone(String value) { + + if (value == null || value.isBlank()) { + + return ""; + + } + + return value.trim(); + + } + +} + + diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentChatService.java b/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentChatService.java new file mode 100644 index 0000000..eadd47b --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentChatService.java @@ -0,0 +1,171 @@ +package com.will.coreAgent.agent.service; + +import com.will.coreAgent.agent.context.AgentOperatorContext; +import com.will.coreAgent.agent.dto.AgentChatRequest; +import com.will.coreAgent.agent.dto.AgentChatResult; +import com.will.coreAgent.framework.mcp.McpContextService; +import com.will.coreCommon.context.UserContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.Map; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class AgentChatService { + + private final ChatClient assistantChatClient; + private final ChatMemory agentChatMemory; + private final McpContextService mcpContextService; + + public AgentChatResult chat(AgentChatRequest request) { + return chat(request, UserContext.getCurrentUserId()); + } + + public AgentChatResult chat(AgentChatRequest request, String operatorUserId) { + validateMessage(request); + String userId = resolveOperator(operatorUserId); + request.setSessionId(resolveSessionId(request.getSessionId())); + String conversationId = buildConversationId(request.getSessionId(), userId); + + AgentOperatorContext.set(userId); + bindUserContext(userId); + long startedAt = System.currentTimeMillis(); + try { + String reply = assistantChatClient.prompt() + .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) + .toolContext(operatorToolContext(userId)) + .user(enrichUserMessage(request.getMessage())) + .call() + .content(); + + log.info("[Agent][Chat] session={}, userId={}, replyLength={}, elapsedMs={}", + request.getSessionId(), + userId, + reply == null ? 0 : reply.length(), + System.currentTimeMillis() - startedAt); + return AgentChatResult.builder() + .sessionId(request.getSessionId()) + .reply(reply == null ? "" : reply) + .build(); + } finally { + AgentOperatorContext.clear(); + } + } + + public Flux streamChat(AgentChatRequest request) { + return streamChat(request, UserContext.getCurrentUserId()); + } + + public Flux streamChat(AgentChatRequest request, String operatorUserId) { + validateMessage(request); + String userId = resolveOperator(operatorUserId); + request.setSessionId(resolveSessionId(request.getSessionId())); + String conversationId = buildConversationId(request.getSessionId(), userId); + + AgentOperatorContext.set(userId); + bindUserContext(userId); + long startedAt = System.currentTimeMillis(); + + return assistantChatClient.prompt() + .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) + .toolContext(operatorToolContext(userId)) + .user(enrichUserMessage(request.getMessage())) + .stream() + .content() + .doOnSubscribe(s -> { + AgentOperatorContext.set(userId); + bindUserContext(userId); + }) + .doOnNext(chunk -> { + AgentOperatorContext.set(userId); + bindUserContext(userId); + }) + .doFinally(signal -> { + log.info("[Agent][Chat] stream service finished, session={}, userId={}, signal={}, elapsedMs={}", + request.getSessionId(), userId, signal, System.currentTimeMillis() - startedAt); + AgentOperatorContext.clear(); + }); + } + + public String ensureSessionId(AgentChatRequest request) { + request.setSessionId(resolveSessionId(request.getSessionId())); + return request.getSessionId(); + } + + public void clearSession(String sessionId) { + if (!StringUtils.hasText(sessionId)) { + return; + } + agentChatMemory.clear(buildConversationId(sessionId, UserContext.getCurrentUserId())); + } + + private Map operatorToolContext(String userId) { + if (!StringUtils.hasText(userId)) { + return Map.of(); + } + return Map.of(AgentOperatorContext.TOOL_CONTEXT_USER_ID, userId); + } + + private String resolveOperator(String operatorUserId) { + if (StringUtils.hasText(operatorUserId)) { + return operatorUserId.trim(); + } + return UserContext.getCurrentUserId(); + } + + private void bindUserContext(String userId) { + if (StringUtils.hasText(userId)) { + UserContext.setCurrentUserId(userId); + } + } + + private void validateMessage(AgentChatRequest request) { + if (request == null || !StringUtils.hasText(request.getMessage())) { + throw new IllegalArgumentException("message 不能为空"); + } + } + + private String resolveSessionId(String sessionId) { + if (StringUtils.hasText(sessionId)) { + return sessionId.trim(); + } + return UUID.randomUUID().toString().replace("-", ""); + } + + private String buildConversationId(String sessionId, String operatorUserId) { + if (StringUtils.hasText(operatorUserId)) { + return operatorUserId + ":" + sessionId; + } + return sessionId; + } + + private String enrichUserMessage(String message) { + LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai")); + LocalDate weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + return """ + 系统时间上下文: + - 当前日期:%s + - 当前时区:Asia/Shanghai + - 本周范围:%s 至 %s(周一到周日) + - 用户提到“本周”时,直接使用上述本周开始日期和结束日期,不要再要求用户提供日期。 + + 用户消息: + %s + """.formatted(today, weekStart, weekEnd, message); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMcpStatusService.java b/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMcpStatusService.java new file mode 100644 index 0000000..4ba01a6 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMcpStatusService.java @@ -0,0 +1,23 @@ +package com.will.coreAgent.agent.service; + +import com.will.coreAgent.agent.dto.McpStatusResult; +import com.will.coreAgent.framework.mcp.McpContextService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class AgentMcpStatusService { + + private final McpContextService mcpContextService; + + public McpStatusResult status() { + return McpStatusResult.builder() + .enabled(mcpContextService.isEnabled()) + .connectedServers(mcpContextService.connectedServerCount()) + .toolNames(mcpContextService.listToolNames()) + .build(); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMetaService.java b/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMetaService.java new file mode 100644 index 0000000..14fa80d --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMetaService.java @@ -0,0 +1,68 @@ +package com.will.coreAgent.agent.service; + +import com.will.coreAgent.agent.dto.AgentDefinition; +import com.will.coreAgent.agent.dto.AgentMetaResult; +import com.will.coreAgent.framework.mcp.McpContextService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class AgentMetaService { + + private final McpContextService mcpContextService; + + @Value("${will.agent.provider:ollama}") + private String provider; + + @Value("${spring.ai.ollama.chat.options.model:qwen2.5:7b}") + private String ollamaModel; + + @Value("${spring.ai.openai.chat.options.model:gpt-4o-mini}") + private String openaiModel; + + @Value("${spring.ai.zhipuai.chat.options.model:glm-4-flash}") + private String zhipuModel; + + public AgentMetaResult getMeta() { + List toolNames = mcpContextService.listToolNames(); + return AgentMetaResult.builder() + .enabled(true) + .provider(provider) + .model(resolveModel()) + .mcpEnabled(mcpContextService.isEnabled()) + .mcpToolCount(toolNames.size()) + .mcpToolNames(toolNames) + .agents(List.of( + AgentDefinition.builder() + .id("chat") + .name("对话助手") + .description("多轮对话,支持部门/用户查询、模块说明") + .endpoint("/api/agent/chat") + .streamingSupported(true) + .build(), + AgentDefinition.builder() + .id("code-gen") + .name("业务代码生成") + .description("基于 LangGraph4j 工作流,自动生成 Entity/Mapper/DTO/Service/Controller") + .endpoint("/api/agent/code/generate") + .streamingSupported(false) + .build() + )) + .build(); + } + + private String resolveModel() { + return switch (provider == null ? "" : provider.trim().toLowerCase()) { + case "openai" -> openaiModel; + case "zhipu" -> zhipuModel; + case "ollama" -> ollamaModel; + default -> ""; + }; + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentAutoConfiguration.java b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentAutoConfiguration.java new file mode 100644 index 0000000..8bb7ab1 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentAutoConfiguration.java @@ -0,0 +1,25 @@ +package com.will.coreAgent.framework.config; + +import com.will.coreAgent.agent.service.AgentChatService; +import com.will.coreAgent.agent.service.AgentMetaService; +import com.will.coreAgent.agent.service.AgentMcpStatusService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.context.annotation.Import; + +@Configuration +@EnableScheduling +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +@Import({ + AiChatModelConfiguration.class, + ZentaoDataSourceConfiguration.class, + AgentMemoryConfiguration.class, + AgentChatClientConfiguration.class, + AgentChatService.class, + AgentMetaService.class, + AgentMcpStatusService.class, + com.will.coreAgent.framework.mcp.McpContextServiceImpl.class, +}) +public class AgentAutoConfiguration { +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentChatClientConfiguration.java b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentChatClientConfiguration.java new file mode 100644 index 0000000..251d8de --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentChatClientConfiguration.java @@ -0,0 +1,46 @@ +package com.will.coreAgent.framework.config; + +import com.will.coreAgent.agent.prompt.WillSystemPrompts; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class AgentChatClientConfiguration { + + /** 代码生成专用:无会话记忆,可调用 MCP 工具 */ + @Bean + public ChatClient agentChatClient( + ChatModel chatModel, + @Autowired(required = false) List toolCallbackProviders) { + ChatClient.Builder builder = ChatClient.builder(chatModel); + if (toolCallbackProviders != null) { + toolCallbackProviders.forEach(builder::defaultToolCallbacks); + } + return builder.build(); + } + + /** 对话助手专用:带会话记忆 + MCP 工具 */ + @Bean + public ChatClient assistantChatClient( + ChatModel chatModel, + ChatMemory agentChatMemory, + @Autowired(required = false) List toolCallbackProviders) { + ChatClient.Builder builder = ChatClient.builder(chatModel) + .defaultSystem(WillSystemPrompts.ASSISTANT_SYSTEM) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(agentChatMemory).build()); + if (toolCallbackProviders != null) { + toolCallbackProviders.forEach(builder::defaultToolCallbacks); + } + return builder.build(); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentMemoryConfiguration.java b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentMemoryConfiguration.java new file mode 100644 index 0000000..8c86e73 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentMemoryConfiguration.java @@ -0,0 +1,23 @@ +package com.will.coreAgent.framework.config; + +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository; +import org.springframework.ai.chat.memory.MessageWindowChatMemory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class AgentMemoryConfiguration { + + @Bean + public ChatMemory agentChatMemory( + @Value("${will.agent.chat.max-messages:30}") int maxMessages) { + return MessageWindowChatMemory.builder() + .chatMemoryRepository(new InMemoryChatMemoryRepository()) + .maxMessages(maxMessages) + .build(); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/config/AiChatModelConfiguration.java b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AiChatModelConfiguration.java new file mode 100644 index 0000000..006be62 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/config/AiChatModelConfiguration.java @@ -0,0 +1,66 @@ +package com.will.coreAgent.framework.config; + +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.api.OllamaOptions; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.zhipuai.ZhiPuAiChatModel; +import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +@Configuration +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class AiChatModelConfiguration { + + @Bean + @ConditionalOnProperty(prefix = "will.agent", name = "provider", havingValue = "openai") + @ConditionalOnMissingBean(ChatModel.class) + public ChatModel openAiChatModel( + @Value("${spring.ai.openai.api-key:}") String apiKey, + @Value("${spring.ai.openai.base-url:https://api.openai.com}") String baseUrl, + @Value("${spring.ai.openai.chat.options.model:gpt-4o-mini}") String model) { + if (!StringUtils.hasText(apiKey)) { + throw new IllegalStateException("will.agent.provider=openai 时需配置 spring.ai.openai.api-key"); + } + return OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder().baseUrl(baseUrl).apiKey(apiKey).build()) + .defaultOptions(OpenAiChatOptions.builder().model(model).temperature(0.2).build()) + .build(); + } + + @Bean + @ConditionalOnProperty(prefix = "will.agent", name = "provider", havingValue = "zhipu") + @ConditionalOnMissingBean(ChatModel.class) + public ChatModel zhipuChatModel( + @Value("${spring.ai.zhipuai.api-key:}") String apiKey, + @Value("${spring.ai.zhipuai.base-url:https://open.bigmodel.cn/api/paas}") String baseUrl, + @Value("${spring.ai.zhipuai.chat.options.model:glm-4-flash}") String model) { + if (!StringUtils.hasText(apiKey)) { + throw new IllegalStateException("will.agent.provider=zhipu 时需配置 spring.ai.zhipuai.api-key"); + } + return new ZhiPuAiChatModel( + new ZhiPuAiApi(baseUrl, apiKey), + ZhiPuAiChatOptions.builder().model(model).temperature(0.2).build()); + } + + @Bean + @ConditionalOnProperty(prefix = "will.agent", name = "provider", havingValue = "ollama", matchIfMissing = true) + @ConditionalOnMissingBean(ChatModel.class) + public ChatModel ollamaChatModel( + @Value("${spring.ai.ollama.base-url:http://localhost:11434}") String baseUrl, + @Value("${spring.ai.ollama.chat.options.model:qwen2.5:7b}") String model) { + return OllamaChatModel.builder() + .ollamaApi(OllamaApi.builder().baseUrl(baseUrl).build()) + .defaultOptions(OllamaOptions.builder().model(model).temperature(0.2).build()) + .build(); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/config/ZentaoDataSourceConfiguration.java b/coreAgent/src/main/java/com/will/coreAgent/framework/config/ZentaoDataSourceConfiguration.java new file mode 100644 index 0000000..f090004 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/config/ZentaoDataSourceConfiguration.java @@ -0,0 +1,29 @@ +package com.will.coreAgent.framework.config; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +@Slf4j +@ConditionalOnProperty(prefix = "will.agent.zentao", name = "enabled", havingValue = "true") +public class ZentaoDataSourceConfiguration { + + @Bean("zentaoDataSource") + @ConfigurationProperties("will.agent.zentao.datasource") + public HikariDataSource zentaoDataSource() { + return new HikariDataSource(); + } + + @Bean("zentaoJdbcTemplate") + public JdbcTemplate zentaoJdbcTemplate(@Qualifier("zentaoDataSource") HikariDataSource dataSource) { + log.info("[Agent][Zentao] datasource enabled, jdbcUrl={}, username={}", + dataSource.getJdbcUrl(), dataSource.getUsername()); + return new JdbcTemplate(dataSource); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextService.java b/coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextService.java new file mode 100644 index 0000000..2fbb843 --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextService.java @@ -0,0 +1,14 @@ +package com.will.coreAgent.framework.mcp; + +import java.util.List; + +public interface McpContextService { + + boolean isEnabled(); + + String buildContextSnippet(String requirement, String moduleName); + + List listToolNames(); + + int connectedServerCount(); +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextServiceImpl.java b/coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextServiceImpl.java new file mode 100644 index 0000000..3f4e14a --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextServiceImpl.java @@ -0,0 +1,133 @@ +package com.will.coreAgent.framework.mcp; + +import io.modelcontextprotocol.spec.McpSchema; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Service +@ConditionalOnProperty(prefix = "will.agent", name = "enabled", havingValue = "true") +public class McpContextServiceImpl implements McpContextService { + + private final boolean mcpEnabled; + private final List toolCallbackProviders; + private final List mcpSyncClients; + + public McpContextServiceImpl( + @Value("${will.agent.mcp.enabled:false}") boolean mcpEnabled, + @Autowired(required = false) List toolCallbackProviders, + @Autowired(required = false) List mcpSyncClients) { + this.mcpEnabled = mcpEnabled; + this.toolCallbackProviders = toolCallbackProviders == null ? List.of() : toolCallbackProviders; + this.mcpSyncClients = mcpSyncClients == null ? List.of() : mcpSyncClients; + } + + @Override + public boolean isEnabled() { + return mcpEnabled && !toolCallbackProviders.isEmpty(); + } + + @Override + public String buildContextSnippet(String requirement, String moduleName) { + if (!isEnabled()) { + return ""; + } + StringBuilder context = new StringBuilder(); + context.append("【MCP 已连接,生成时可调用外部工具】\n"); + appendTools(context); + appendResourcesIfSupported(context); + context.append(""" + - 建议优先使用 MCP 工具读取项目现有代码风格,参考模块:%s + - 业务需求:%s + """.formatted(moduleName, requirement)); + return context.toString(); + } + + @Override + public List listToolNames() { + if (!isEnabled()) { + return Collections.emptyList(); + } + List names = new ArrayList<>(); + for (ToolCallback callback : allToolCallbacks()) { + names.add(callback.getToolDefinition().name()); + } + return names; + } + + @Override + public int connectedServerCount() { + return isEnabled() ? mcpSyncClients.size() : 0; + } + + private List allToolCallbacks() { + List callbacks = new ArrayList<>(); + for (ToolCallbackProvider provider : toolCallbackProviders) { + Collections.addAll(callbacks, provider.getToolCallbacks()); + } + return callbacks; + } + + private void appendTools(StringBuilder context) { + List callbacks = allToolCallbacks(); + if (callbacks.isEmpty()) { + context.append("- 可用 MCP 工具:无\n"); + return; + } + context.append("- 可用 MCP 工具(").append(callbacks.size()).append(" 个):\n"); + for (ToolCallback callback : callbacks) { + context.append(" * ").append(callback.getToolDefinition().name()); + if (StringUtils.hasText(callback.getToolDefinition().description())) { + context.append(" — ").append(callback.getToolDefinition().description()); + } + context.append('\n'); + } + } + + private void appendResourcesIfSupported(StringBuilder context) { + if (mcpSyncClients.isEmpty()) { + context.append("- MCP Server 连接数:0\n"); + return; + } + context.append("- MCP Server 连接数:").append(mcpSyncClients.size()).append('\n'); + for (io.modelcontextprotocol.client.McpSyncClient client : mcpSyncClients) { + try { + McpSchema.ListResourcesResult resources = client.listResources(); + if (resources == null || resources.resources() == null || resources.resources().isEmpty()) { + continue; + } + context.append(" 资源列表:\n"); + int limit = Math.min(resources.resources().size(), 8); + for (int i = 0; i < limit; i++) { + McpSchema.Resource resource = resources.resources().get(i); + context.append(" - ").append(resource.uri()); + if (StringUtils.hasText(resource.name())) { + context.append(" (").append(resource.name()).append(')'); + } + context.append('\n'); + } + } catch (Exception ex) { + if (isResourcesNotSupported(ex)) { + log.debug("[Agent][MCP] server has no resources capability, skip"); + } else { + log.debug("[Agent][MCP] listResources skipped: {}", ex.getMessage()); + } + } + } + } + + private static boolean isResourcesNotSupported(Throwable ex) { + String message = ex.getMessage(); + return message != null && message.toLowerCase().contains("resources"); + } +} diff --git a/coreAgent/src/main/java/com/will/coreAgent/framework/tool/UserContextAwareToolCallbackProvider.java b/coreAgent/src/main/java/com/will/coreAgent/framework/tool/UserContextAwareToolCallbackProvider.java new file mode 100644 index 0000000..5197b9c --- /dev/null +++ b/coreAgent/src/main/java/com/will/coreAgent/framework/tool/UserContextAwareToolCallbackProvider.java @@ -0,0 +1,72 @@ +package com.will.coreAgent.framework.tool; + +import com.will.coreAgent.agent.context.AgentOperatorContext; +import com.will.coreCommon.context.UserContext; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.definition.ToolDefinition; +import org.springframework.util.StringUtils; + +/** + * 包装 ToolCallback:执行前从 toolContext / AgentOperatorContext 恢复登录用户。 + */ +public final class UserContextAwareToolCallbackProvider implements ToolCallbackProvider { + + private final ToolCallbackProvider delegate; + + public UserContextAwareToolCallbackProvider(ToolCallbackProvider delegate) { + this.delegate = delegate; + } + + @Override + public ToolCallback[] getToolCallbacks() { + ToolCallback[] origin = delegate.getToolCallbacks(); + ToolCallback[] wrapped = new ToolCallback[origin.length]; + for (int i = 0; i < origin.length; i++) { + wrapped[i] = wrap(origin[i]); + } + return wrapped; + } + + private static ToolCallback wrap(ToolCallback origin) { + return new ToolCallback() { + @Override + public ToolDefinition getToolDefinition() { + return origin.getToolDefinition(); + } + + @Override + public String call(String toolInput) { + bindUser(null); + return origin.call(toolInput); + } + + @Override + public String call(String toolInput, ToolContext toolContext) { + bindUser(toolContext); + return origin.call(toolInput, toolContext); + } + }; + } + + private static void bindUser(ToolContext toolContext) { + String userId = null; + if (toolContext != null && toolContext.getContext() != null) { + Object value = toolContext.getContext().get(AgentOperatorContext.TOOL_CONTEXT_USER_ID); + if (value != null) { + userId = String.valueOf(value); + } + } + if (!StringUtils.hasText(userId)) { + userId = AgentOperatorContext.get(); + } + if (!StringUtils.hasText(userId)) { + userId = UserContext.getCurrentUserId(); + } + if (StringUtils.hasText(userId)) { + UserContext.setCurrentUserId(userId); + AgentOperatorContext.set(userId); + } + } +} diff --git a/coreAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/coreAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..a6efd28 --- /dev/null +++ b/coreAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.will.coreAgent.framework.config.AgentAutoConfiguration diff --git a/coreApi/build.gradle b/coreApi/build.gradle new file mode 100644 index 0000000..d52f70e --- /dev/null +++ b/coreApi/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(':coreCommon') +// implementation project(':coreData') + implementation project(':coreService') + implementation project(':coreAuth') + implementation project(':coreAgent') + implementation project(':coreJob') + implementation("org.springframework.boot:spring-boot-starter-web") +} + +test { + useJUnitPlatform() +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AgentChatController.java b/coreApi/src/main/java/com/will/coreApi/controller/AgentChatController.java new file mode 100644 index 0000000..7d94f86 --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AgentChatController.java @@ -0,0 +1,194 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreAgent.agent.dto.AgentChatRequest; +import com.will.coreAgent.agent.dto.AgentChatResult; +import com.will.coreAgent.agent.dto.AgentMetaResult; +import com.will.coreAgent.agent.service.AgentChatService; +import com.will.coreAgent.agent.service.AgentMetaService; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.context.UserContext; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +@Slf4j +@RestController +@RequestMapping("/api/agent") +@RequiredArgsConstructor +public class AgentChatController { + + private final ObjectProvider agentChatService; + private final ObjectProvider agentMetaService; + + @RequirePermission(ApiPermissions.AGENT_LIST) + @GetMapping("/meta") + public ResponseMessage meta() { + AgentMetaService service = agentMetaService.getIfAvailable(); + if (service == null) { + return ResponseMessageFactory.error("AI Agent 未启用"); + } + return ResponseMessageFactory.success(service.getMeta()); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @PostMapping("/chat") + public ResponseMessage chat(@RequestBody AgentChatRequest request) { + AgentChatService service = agentChatService.getIfAvailable(); + if (service == null) { + return ResponseMessageFactory.error("AI Agent 未启用"); + } + String operatorUserId = resolveOperatorUserId(); + return ResponseMessageFactory.success(service.chat(request, operatorUserId)); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public void streamChat(@RequestBody AgentChatRequest request, HttpServletResponse response) { + long startedAt = System.currentTimeMillis(); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE); + response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + response.setHeader(HttpHeaders.CONNECTION, "keep-alive"); + response.setHeader("X-Accel-Buffering", "no"); + + AgentChatService service = agentChatService.getIfAvailable(); + // 在进入异步/流式前快照,避免 ThreadLocal 丢失 + String operatorUserId = resolveOperatorUserId(); + PrintWriter out = null; + + try { + out = response.getWriter(); + final PrintWriter writer = out; + if (StringUtils.hasText(operatorUserId)) { + UserContext.setCurrentUserId(operatorUserId); + } + + if (service == null) { + safeWriteSse(writer, "error", "AI Agent 未启用"); + return; + } + if (request == null || !StringUtils.hasText(request.getMessage())) { + safeWriteSse(writer, "error", "message 不能为空"); + return; + } + + String sessionId = service.ensureSessionId(request); + safeWriteSse(writer, "session", sessionId); + + try { + CountDownLatch done = new CountDownLatch(1); + AtomicReference streamError = new AtomicReference<>(); + AtomicReference firstChunkAt = new AtomicReference<>(); + service.streamChat(request, operatorUserId) + .doOnNext(chunk -> { + firstChunkAt.compareAndSet(null, System.currentTimeMillis()); + safeWriteSse(writer, "message", chunk); + }) + .doOnError(ex -> { + streamError.set(ex); + safeWriteSse(writer, "error", resolveErrorMessage(ex)); + done.countDown(); + }) + .doOnComplete(done::countDown) + .subscribe(); + done.await(); + Throwable ex = streamError.get(); + if (ex != null) { + log.warn("[Agent][Chat] stream failed, elapsedMs={}", System.currentTimeMillis() - startedAt, ex); + } else { + Long first = firstChunkAt.get(); + log.info("[Agent][Chat] stream done, firstChunkMs={}, elapsedMs={}", + first == null ? null : first - startedAt, + System.currentTimeMillis() - startedAt); + } + } catch (Exception ex) { + log.warn("[Agent][Chat] stream failed, elapsedMs={}", System.currentTimeMillis() - startedAt, ex); + safeWriteSse(writer, "error", resolveErrorMessage(ex)); + } + } catch (Exception ex) { + log.warn("[Agent][Chat] stream failed, elapsedMs={}", System.currentTimeMillis() - startedAt, ex); + if (out != null) { + safeWriteSse(out, "error", resolveErrorMessage(ex)); + } + } finally { + if (StringUtils.hasText(operatorUserId)) { + UserContext.setCurrentUserId(operatorUserId); + } + } + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @DeleteMapping("/chat/session/{sessionId}") + public ResponseMessage clearSession(@PathVariable String sessionId) { + AgentChatService service = agentChatService.getIfAvailable(); + if (service == null) { + return ResponseMessageFactory.error("AI Agent 未启用"); + } + service.clearSession(sessionId); + return ResponseMessageFactory.success("ok"); + } + + /** @return false 表示客户端已断开,不应继续写入 */ + private static boolean safeWriteSse(PrintWriter writer, String event, String data) { + if (writer == null) { + return false; + } + try { + writer.write("event: " + event + "\n"); + String payload = data == null ? "" : data; + for (String line : payload.split("\n", -1)) { + writer.write("data: " + line + "\n"); + } + writer.write("\n"); + writer.flush(); + return !writer.checkError(); + } catch (Exception ex) { + return false; + } + } + + private static String resolveOperatorUserId() { + // UserContext 已在拦截器写入,并可回退到 Sa-Token Resolver + return UserContext.getCurrentUserId(); + } + + private static String resolveErrorMessage(Throwable ex) { + Throwable current = ex; + while (current != null) { + String message = current.getMessage(); + if (StringUtils.hasText(message)) { + if (message.contains("429") || message.toLowerCase().contains("balance")) { + return "大模型余额不足或请求过于频繁,请稍后重试或更换模型"; + } + if (message.contains("401") || message.toLowerCase().contains("api key")) { + return "大模型 API Key 无效,请检查 application.yml 配置"; + } + return message; + } + current = current.getCause(); + } + if (ex != null) { + return "对话失败:" + ex.getClass().getSimpleName(); + } + return "对话失败,请稍后重试"; + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AgentChatHistoryController.java b/coreApi/src/main/java/com/will/coreApi/controller/AgentChatHistoryController.java new file mode 100644 index 0000000..cf514de --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AgentChatHistoryController.java @@ -0,0 +1,71 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.dto.AgentChatMessageDTO; +import com.will.coreService.dto.AgentChatMessageSaveDTO; +import com.will.coreService.dto.AgentChatSessionDTO; +import com.will.coreService.dto.AgentChatSessionSaveDTO; +import com.will.coreService.service.AgentChatHistoryService; +import lombok.RequiredArgsConstructor; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/agent/chat/history") +@RequiredArgsConstructor +public class AgentChatHistoryController { + + private final AgentChatHistoryService agentChatHistoryService; + + @RequirePermission(ApiPermissions.AGENT_LIST) + @GetMapping("/sessions") + public ResponseMessage> sessions( + @RequestParam(value = "limit", required = false) Integer limit) { + return ResponseMessageFactory.success(agentChatHistoryService.listSessions(limit)); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @PostMapping("/sessions") + public ResponseMessage saveSession(@RequestBody(required = false) AgentChatSessionSaveDTO dto) { + return ResponseMessageFactory.success(agentChatHistoryService.saveSession(dto)); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @GetMapping("/sessions/{sessionId}") + public ResponseMessage detail(@PathVariable String sessionId) { + return ResponseMessageFactory.success(agentChatHistoryService.detail(sessionId)); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @PostMapping("/sessions/{sessionId}/messages") + public ResponseMessage appendMessage( + @PathVariable String sessionId, + @RequestBody AgentChatMessageSaveDTO dto) { + return ResponseMessageFactory.success(agentChatHistoryService.appendMessage(sessionId, dto)); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @DeleteMapping("/sessions/{sessionId}/messages") + public ResponseMessage clearMessages(@PathVariable String sessionId) { + agentChatHistoryService.clearMessages(sessionId); + return ResponseMessageFactory.success("ok"); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @DeleteMapping("/sessions/{sessionId}") + public ResponseMessage deleteSession(@PathVariable String sessionId) { + agentChatHistoryService.deleteSession(sessionId); + return ResponseMessageFactory.success("ok"); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AgentMcpController.java b/coreApi/src/main/java/com/will/coreApi/controller/AgentMcpController.java new file mode 100644 index 0000000..fe51e7e --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AgentMcpController.java @@ -0,0 +1,37 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreAgent.agent.dto.McpStatusResult; +import com.will.coreAgent.agent.service.AgentMcpStatusService; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/agent/mcp") +@RequiredArgsConstructor +public class AgentMcpController { + + private final ObjectProvider agentMcpStatusService; + + @RequirePermission(ApiPermissions.AGENT_LIST) + @GetMapping("/status") + public ResponseMessage status() { + AgentMcpStatusService service = agentMcpStatusService.getIfAvailable(); + if (service != null) { + return ResponseMessageFactory.success(service.status()); + } + return ResponseMessageFactory.success(McpStatusResult.builder() + .enabled(false) + .connectedServers(0) + .toolNames(List.of()) + .build()); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AgentReportController.java b/coreApi/src/main/java/com/will/coreApi/controller/AgentReportController.java new file mode 100644 index 0000000..97829ac --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AgentReportController.java @@ -0,0 +1,76 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.dto.AgentReportConfigDTO; +import com.will.coreService.dto.AgentReportJobConfigDTO; +import com.will.coreService.dto.AgentReportJobTypeDTO; +import com.will.coreService.service.AgentReportConfigService; +import com.will.coreService.service.AgentReportJobConfigService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +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; + +@RestController +@RequestMapping("/api/agent/report") +@RequiredArgsConstructor +public class AgentReportController { + + private final AgentReportConfigService agentReportConfigService; + private final AgentReportJobConfigService agentReportJobConfigService; + + @RequirePermission(ApiPermissions.AGENT_LIST) + @GetMapping("/config") + public ResponseMessage config() { + return ResponseMessageFactory.success(agentReportConfigService.getConfig()); + } + + @RequirePermission(ApiPermissions.AGENT_LIST) + @PostMapping("/config") + public ResponseMessage saveConfig(@RequestBody AgentReportConfigDTO dto) { + return ResponseMessageFactory.success(agentReportConfigService.saveConfig(dto)); + } + + @RequirePermission(ApiPermissions.SCHEDULE_JOB_LIST) + @GetMapping("/jobs") + public ResponseMessage> jobs( + @RequestParam(required = false) String name, + @RequestParam(required = false) String jobType, + @RequestParam(required = false) Boolean enabled) { + return ResponseMessageFactory.success(agentReportJobConfigService.list(name, jobType, enabled)); + } + + @RequirePermission(ApiPermissions.SCHEDULE_JOB_LIST) + @GetMapping("/job-types") + public ResponseMessage> jobTypes() { + return ResponseMessageFactory.success(agentReportJobConfigService.jobTypes()); + } + + @RequirePermission(ApiPermissions.SCHEDULE_JOB_SAVE) + @PostMapping("/jobs") + public ResponseMessage saveJob(@RequestBody AgentReportJobConfigDTO dto) { + return ResponseMessageFactory.success(agentReportJobConfigService.save(dto)); + } + + @RequirePermission(ApiPermissions.SCHEDULE_JOB_DELETE) + @DeleteMapping("/jobs/{id}") + public ResponseMessage deleteJob(@PathVariable String id) { + agentReportJobConfigService.delete(id); + return ResponseMessageFactory.success("ok"); + } + + @RequirePermission(ApiPermissions.SCHEDULE_JOB_EXECUTE) + @PostMapping("/jobs/{id}/execute") + public ResponseMessage executeJob(@PathVariable String id) { + return ResponseMessageFactory.success(agentReportJobConfigService.execute(id)); + } + +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AgentTaskFunctionController.java b/coreApi/src/main/java/com/will/coreApi/controller/AgentTaskFunctionController.java new file mode 100644 index 0000000..e48329f --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AgentTaskFunctionController.java @@ -0,0 +1,47 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.dto.AgentTaskFunctionDTO; +import com.will.coreService.service.AgentTaskFunctionService; +import lombok.RequiredArgsConstructor; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/config/task-functions") +@RequiredArgsConstructor +public class AgentTaskFunctionController { + + private final AgentTaskFunctionService taskFunctionService; + + @RequirePermission(ApiPermissions.TASK_FUNCTION_LIST) + @GetMapping + public ResponseMessage> list( + @RequestParam(required = false) String name, + @RequestParam(required = false) String code, + @RequestParam(required = false) Boolean enabled) { + return ResponseMessageFactory.success(taskFunctionService.list(name, code, enabled)); + } + + @RequirePermission(ApiPermissions.TASK_FUNCTION_SAVE) + @PostMapping + public ResponseMessage save(@RequestBody AgentTaskFunctionDTO dto) { + return ResponseMessageFactory.success(taskFunctionService.save(dto)); + } + + @RequirePermission(ApiPermissions.TASK_FUNCTION_DELETE) + @DeleteMapping("/{id}") + public ResponseMessage delete(@PathVariable String id) { + taskFunctionService.delete(id); + return ResponseMessageFactory.success("ok"); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AuthController.java b/coreApi/src/main/java/com/will/coreApi/controller/AuthController.java new file mode 100644 index 0000000..8219b89 --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AuthController.java @@ -0,0 +1,38 @@ +package com.will.coreApi.controller; + +import com.will.auth.service.AuthService; +import com.will.coreCommon.general.ResponseMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public ResponseMessage login(@RequestBody Map body) { + String account = body.getOrDefault("account", body.get("username")); + return authService.login(account, body.get("password")); + } + + @PostMapping("/logout") + public ResponseMessage logout() { + return authService.logout(); + } + + @GetMapping("/codes") + public ResponseMessage> codes() { + return authService.getAccessCodes(); + } + + @GetMapping("/user/info") + public ResponseMessage> userInfo() { + return authService.getUserInfo(); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AuthDataPermissionController.java b/coreApi/src/main/java/com/will/coreApi/controller/AuthDataPermissionController.java new file mode 100644 index 0000000..55a8740 --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AuthDataPermissionController.java @@ -0,0 +1,40 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.dto.DataPermissionDTO; +import com.will.coreService.dto.DataPermissionListItemDTO; +import com.will.coreService.dto.DataPermissionSaveDTO; +import com.will.coreService.service.DataAuthService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/sys/data-permission") +@RequiredArgsConstructor +public class AuthDataPermissionController { + + private final DataAuthService dataAuthService; + + @RequirePermission(ApiPermissions.DATA_PERM_LIST) + @GetMapping("/list") + public ResponseMessage> list() { + return ResponseMessageFactory.success(dataAuthService.listAll()); + } + + @RequirePermission(ApiPermissions.DATA_PERM_LIST) + @GetMapping("/role/{roleId}") + public ResponseMessage> listByRole(@PathVariable String roleId) { + return ResponseMessageFactory.success(dataAuthService.listByRoleId(roleId)); + } + + @RequirePermission(ApiPermissions.DATA_PERM_SAVE) + @PostMapping("/save") + public ResponseMessage save(@RequestBody DataPermissionSaveDTO dto) { + return dataAuthService.save(dto); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AuthDepartmentController.java b/coreApi/src/main/java/com/will/coreApi/controller/AuthDepartmentController.java new file mode 100644 index 0000000..740b6ef --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AuthDepartmentController.java @@ -0,0 +1,183 @@ +package com.will.coreApi.controller; + + + +import com.will.auth.annotation.RequirePermission; + +import com.will.coreCommon.auth.ApiPermissions; + +import com.will.coreService.dto.DepartmentDTO; + +import com.will.coreService.dto.UserDTO; + +import com.will.coreCommon.BaseListDTO; + +import com.will.coreCommon.BasePage; + +import com.will.coreCommon.general.ResponseMessage; + +import com.will.coreCommon.general.ResponseMessageFactory; + +import com.will.coreService.service.DepartmentService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.web.bind.annotation.*; + + + +import java.util.List; + +import java.util.Map; + +import java.util.stream.Collectors; + + + +@RequiredArgsConstructor + +@RestController + +@RequestMapping("/api/sys/department") + +public class AuthDepartmentController { + + + + private final DepartmentService departmentService; + + + + @RequirePermission(ApiPermissions.DEPT_TEST) + + @RequestMapping("/test") + + public String test() { + + return "test"; + + } + + + + @RequirePermission(ApiPermissions.DEPT_TABLE_LIST) + + @RequestMapping("/table/list") + + public ResponseMessage> tableList( + + @RequestParam("pageIndex") String pageIndex, + + @RequestParam("pageSize") String pageSize, + + @RequestParam(value = "departmentName", required = false) String departmentName) { + + Integer index = Integer.parseInt(pageIndex); + + Integer size = Integer.parseInt(pageSize); + + DepartmentDTO departmentDto = new DepartmentDTO(); + + departmentDto.setDepartmentName(departmentName); + + return departmentService.getDepartmentPageDto(departmentDto, index, size); + + } + + + + @RequirePermission(ApiPermissions.DEPT_LIST) + + @RequestMapping("/list") + + public ResponseMessage> list() { + + return departmentService.getDepartmentListDto(); + + } + + + + @RequirePermission(ApiPermissions.DEPT_SAVE) + + @RequestMapping("/save") + + public ResponseMessage save(@RequestBody DepartmentDTO departmentDto) { + + return departmentService.saveDepartment(departmentDto); + + } + + + + @RequirePermission(ApiPermissions.DEPT_DELETE) + + @RequestMapping("/delete") + + public ResponseMessage delete(@RequestBody Map idsMap) { + + Object idsObj = idsMap.get("ids"); + + if (!(idsObj instanceof List tempList)) { + + return ResponseMessageFactory.error("参数格式错误"); + + } + + + + List ids = tempList.stream() + + .filter(item -> item instanceof String) + + .map(Object::toString) + + .collect(Collectors.toList()); + + + + if (ids.size() != tempList.size()) { + + return ResponseMessageFactory.error("参数格式错误"); + + } + + + + boolean delete = departmentService.removeBatchByIds(ids); + + if (delete) { + + return ResponseMessageFactory.success(); + + } + + return ResponseMessageFactory.error("删除失败"); + + } + + + + @RequirePermission(ApiPermissions.DEPT_USERS) + + @RequestMapping("/users") + + public ResponseMessage> userList( + + @RequestParam("pageIndex") String pageIndex, + + @RequestParam("pageSize") String pageSize, + + @RequestParam("id") String id) { + + Integer index = Integer.parseInt(pageIndex); + + Integer size = Integer.parseInt(pageSize); + + return departmentService.getPageUsersByDepartmentId(id, index, size); + + } + +} + + diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AuthMenuController.java b/coreApi/src/main/java/com/will/coreApi/controller/AuthMenuController.java new file mode 100644 index 0000000..4f249af --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AuthMenuController.java @@ -0,0 +1,54 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.BaseListDTO; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.dto.MenuDTO; +import com.will.coreService.service.MenuManageService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/sys/menu") +@RequiredArgsConstructor +public class AuthMenuController { + + private final MenuManageService menuManageService; + + @RequirePermission(ApiPermissions.MENU_LIST) + @GetMapping("/list") + public ResponseMessage> list() { + return menuManageService.listTree(); + } + + @RequirePermission(ApiPermissions.MENU_SAVE) + @PostMapping("/save") + public ResponseMessage save(@RequestBody MenuDTO menuDTO) { + return menuManageService.save(menuDTO); + } + + @RequirePermission(ApiPermissions.MENU_DELETE) + @PostMapping("/delete") + public ResponseMessage delete(@RequestBody Map idsMap) { + Object idsObj = idsMap.get("ids"); + if (!(idsObj instanceof List tempList)) { + return ResponseMessageFactory.error("参数格式错误"); + } + List ids = tempList.stream() + .filter(String.class::isInstance) + .map(Object::toString) + .collect(Collectors.toList()); + if (ids.size() != tempList.size()) { + return ResponseMessageFactory.error("参数格式错误"); + } + return menuManageService.removeBatchByIds(ids) + ? ResponseMessageFactory.success() + : ResponseMessageFactory.error("删除失败"); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AuthPermissionInfoController.java b/coreApi/src/main/java/com/will/coreApi/controller/AuthPermissionInfoController.java new file mode 100644 index 0000000..808dc23 --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AuthPermissionInfoController.java @@ -0,0 +1,23 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/sys/permission-info") +public class AuthPermissionInfoController { + + @RequirePermission(ApiPermissions.PERM_INFO_LIST) + @GetMapping("/list") + public ResponseMessage>> list() { + return ResponseMessageFactory.success(List.of()); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AuthRoleController.java b/coreApi/src/main/java/com/will/coreApi/controller/AuthRoleController.java new file mode 100644 index 0000000..ab8f6d7 --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AuthRoleController.java @@ -0,0 +1,68 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.BaseListDTO; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.dto.RoleDTO; +import com.will.coreService.service.RoleService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/sys/role") +@RequiredArgsConstructor +public class AuthRoleController { + + private final RoleService roleService; + + @RequirePermission(ApiPermissions.ROLE_LIST) + @GetMapping("/list") + public ResponseMessage> list() { + return roleService.list(); + } + + @RequirePermission(ApiPermissions.ROLE_LIST) + @GetMapping("/{id}") + public ResponseMessage detail(@PathVariable String id) { + return roleService.detail(id); + } + + @RequirePermission(ApiPermissions.ROLE_SAVE) + @PostMapping("/save") + public ResponseMessage save(@RequestBody RoleDTO roleDTO) { + return roleService.save(roleDTO); + } + + @RequirePermission(ApiPermissions.ROLE_ASSIGN) + @PostMapping("/{id}/menus") + public ResponseMessage assignMenus( + @PathVariable String id, + @RequestBody Map> body) { + return roleService.assignMenus(id, body.get("menuIds")); + } + + @RequirePermission(ApiPermissions.ROLE_DELETE) + @PostMapping("/delete") + public ResponseMessage delete(@RequestBody Map idsMap) { + Object idsObj = idsMap.get("ids"); + if (!(idsObj instanceof List tempList)) { + return ResponseMessageFactory.error("参数格式错误"); + } + List ids = tempList.stream() + .filter(String.class::isInstance) + .map(Object::toString) + .collect(Collectors.toList()); + if (ids.size() != tempList.size()) { + return ResponseMessageFactory.error("参数格式错误"); + } + return roleService.removeBatchByIds(ids) + ? ResponseMessageFactory.success() + : ResponseMessageFactory.error("删除失败"); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/AuthUserController.java b/coreApi/src/main/java/com/will/coreApi/controller/AuthUserController.java new file mode 100644 index 0000000..d42f1b7 --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/AuthUserController.java @@ -0,0 +1,69 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.BasePage; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.dto.UserDTO; +import com.will.coreService.dto.UserSaveDTO; +import com.will.coreService.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/sys/user") +@RequiredArgsConstructor +public class AuthUserController { + + private final UserService userService; + + @RequirePermission(ApiPermissions.USER_LIST) + @GetMapping("/list") + public ResponseMessage> list( + @RequestParam(value = "pageIndex", defaultValue = "1") Integer pageIndex, + @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize, + @RequestParam(value = "account", required = false) String account) { + return userService.page(account, pageIndex, pageSize); + } + + @RequirePermission(ApiPermissions.USER_LIST) + @GetMapping("/{id}") + public ResponseMessage detail(@PathVariable String id) { + return userService.detail(id); + } + + @RequirePermission(ApiPermissions.USER_SAVE) + @PostMapping("/save") + public ResponseMessage save(@RequestBody UserSaveDTO userSaveDTO) { + return userService.save(userSaveDTO); + } + + @RequirePermission(ApiPermissions.USER_DELETE) + @PostMapping("/delete") + public ResponseMessage delete(@RequestBody Map idsMap) { + List ids = extractIds(idsMap); + if (ids == null) { + return ResponseMessageFactory.error("参数格式错误"); + } + return userService.removeBatchByIds(ids) + ? ResponseMessageFactory.success() + : ResponseMessageFactory.error("删除失败"); + } + + private List extractIds(Map idsMap) { + Object idsObj = idsMap.get("ids"); + if (!(idsObj instanceof List tempList)) { + return null; + } + List ids = tempList.stream() + .filter(String.class::isInstance) + .map(Object::toString) + .collect(Collectors.toList()); + return ids.size() == tempList.size() ? ids : null; + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/MenuController.java b/coreApi/src/main/java/com/will/coreApi/controller/MenuController.java new file mode 100644 index 0000000..ac880bc --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/MenuController.java @@ -0,0 +1,27 @@ +package com.will.coreApi.controller; + +import com.will.coreCommon.context.UserContext; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreService.service.MenuManageService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/menu") +@RequiredArgsConstructor +public class MenuController { + + private final MenuManageService menuManageService; + + @GetMapping("/all") + public ResponseMessage>> all() { + return ResponseMessageFactory.success( + menuManageService.getUserMenus(UserContext.getCurrentUserId())); + } +} diff --git a/coreApi/src/main/java/com/will/coreApi/controller/TableCommonController.java b/coreApi/src/main/java/com/will/coreApi/controller/TableCommonController.java new file mode 100644 index 0000000..ee5fb28 --- /dev/null +++ b/coreApi/src/main/java/com/will/coreApi/controller/TableCommonController.java @@ -0,0 +1,24 @@ +package com.will.coreApi.controller; + +import com.will.auth.annotation.RequirePermission; +import com.will.coreCommon.auth.ApiPermissions; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/common") +public class TableCommonController { + + @RequirePermission(ApiPermissions.COMMON_TABLE_LIST) + @RequestMapping("/{tableCode}/table/list") + public String list(@PathVariable String tableCode) { + return "test"; + } + + @RequirePermission(ApiPermissions.COMMON_TABLE_GET) + @RequestMapping("/{tableCode}/table/get/{id}") + public String get(@PathVariable String tableCode, @PathVariable String id) { + return "get"; + } +} diff --git a/coreAuth/build.gradle b/coreAuth/build.gradle new file mode 100644 index 0000000..a40f1da --- /dev/null +++ b/coreAuth/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(":coreCommon") + implementation project(":coreData") + implementation("cn.dev33:sa-token-jwt:1.44.0") + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation("cn.dev33:sa-token-spring-boot3-starter:1.44.0") +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/coreAuth/src/main/java/com/will/auth/annotation/RequirePermission.java b/coreAuth/src/main/java/com/will/auth/annotation/RequirePermission.java new file mode 100644 index 0000000..9a4b55f --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/annotation/RequirePermission.java @@ -0,0 +1,13 @@ +package com.will.auth.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequirePermission { + String value(); +} diff --git a/coreAuth/src/main/java/com/will/auth/annotation/RequireRole.java b/coreAuth/src/main/java/com/will/auth/annotation/RequireRole.java new file mode 100644 index 0000000..ac2d03a --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/annotation/RequireRole.java @@ -0,0 +1,11 @@ +package com.will.auth.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(java.lang.annotation.RetentionPolicy.RUNTIME) +public @interface RequireRole { + String[] value() default {}; +} diff --git a/coreAuth/src/main/java/com/will/auth/aspect/PermissionCheckAspect.java b/coreAuth/src/main/java/com/will/auth/aspect/PermissionCheckAspect.java new file mode 100644 index 0000000..70b82b1 --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/aspect/PermissionCheckAspect.java @@ -0,0 +1,40 @@ +package com.will.auth.aspect; + +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import cn.dev33.satoken.stp.StpUtil; +import com.will.auth.annotation.RequirePermission; +import com.will.auth.annotation.RequireRole; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Slf4j +@Component +@Aspect +public class PermissionCheckAspect { + + @Around("@annotation(requirePermission)") + public Object checkPermission(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable { + String permission = requirePermission.value(); + if (!StpUtil.hasPermission(permission)) { + log.warn("权限不足:用户 {} 缺少权限 {}", StpUtil.getLoginIdDefaultNull(), permission); + throw new NotPermissionException(permission); + } + return joinPoint.proceed(); + } + + @Around("@annotation(requireRole)") + public Object checkRole(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable { + String[] roles = requireRole.value(); + if (!StpUtil.hasRoleOr(roles)) { + log.warn("角色不足:用户 {} 不在角色 {} 中", StpUtil.getLoginIdDefaultNull(), Arrays.toString(roles)); + throw new NotRoleException(roles[0]); + } + return joinPoint.proceed(); + } +} diff --git a/coreAuth/src/main/java/com/will/auth/config/AuthAutoConfiguration.java b/coreAuth/src/main/java/com/will/auth/config/AuthAutoConfiguration.java new file mode 100644 index 0000000..18a7edc --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/config/AuthAutoConfiguration.java @@ -0,0 +1,31 @@ +package com.will.auth.config; + +import com.will.auth.interceptor.ApiPrefixGuardInterceptor; +import com.will.auth.interceptor.UserContextInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@EnableAspectJAutoProxy +public class AuthAutoConfiguration { + + @Bean + public WebMvcConfigurer webMvcConfigurer( + ApiPrefixGuardInterceptor apiPrefixGuardInterceptor, + UserContextInterceptor interceptor) { + return new WebMvcConfigurer() { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(apiPrefixGuardInterceptor) + .addPathPatterns("/**"); + + registry.addInterceptor(interceptor) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/auth/login", "/api/auth/logout"); + } + }; + } +} diff --git a/coreAuth/src/main/java/com/will/auth/config/StpInterfaceImpl.java b/coreAuth/src/main/java/com/will/auth/config/StpInterfaceImpl.java new file mode 100644 index 0000000..2a84568 --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/config/StpInterfaceImpl.java @@ -0,0 +1,25 @@ +package com.will.auth.config; + +import cn.dev33.satoken.stp.StpInterface; +import com.will.coreData.auth.AuthRbacSupport; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class StpInterfaceImpl implements StpInterface { + + private final AuthRbacSupport authRbacSupport; + + @Override + public List getPermissionList(Object loginId, String loginType) { + return authRbacSupport.getPermissionCodes(String.valueOf(loginId)); + } + + @Override + public List getRoleList(Object loginId, String loginType) { + return authRbacSupport.getRoleCodes(String.valueOf(loginId)); + } +} diff --git a/coreAuth/src/main/java/com/will/auth/config/SysSessionParameter.java b/coreAuth/src/main/java/com/will/auth/config/SysSessionParameter.java new file mode 100644 index 0000000..fc5156a --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/config/SysSessionParameter.java @@ -0,0 +1,43 @@ +package com.will.auth.config; + +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + + +public class SysSessionParameter extends SaLoginParameter { + String userCode; + String datasource; + String encryptionSecret; // aes 密钥的一部分 + String device; // 设备信息 + boolean readOnly; + String ipAddress; + + public SysSessionParameter createForRequest() { + HttpServletRequest request = null; + RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); + if (attrs == null) { + return null; + } else { + request = ((ServletRequestAttributes) attrs).getRequest(); + } + + this.userCode = request.getHeader(SysSessionSetting.HEADER_USER); + this.datasource = request.getHeader(SysSessionSetting.HEADER_DATASOURCE); + this.readOnly = Boolean.parseBoolean(request.getHeader(SysSessionSetting.HEADER_DATASOURCE_READ_ONLY)); + this.ipAddress = request.getRemoteAddr(); + this.device = request.getHeader(SysSessionSetting.HEADER_DEVICE_TYPE); + this.encryptionSecret = request.getHeader(SysSessionSetting.HEADER_ENCRYPTION_SECRET); + + this.setExtra(SysSessionSetting.HEADER_USER, this.userCode); + this.setExtra(SysSessionSetting.HEADER_DATASOURCE, this.datasource); + this.setExtra(SysSessionSetting.HEADER_DATASOURCE_READ_ONLY, this.readOnly); + this.setExtra(SysSessionSetting.HEADER_IP_ADDRESS, this.ipAddress); + this.setExtra(SysSessionSetting.HEADER_DEVICE_TYPE, this.device); + this.setExtra(SysSessionSetting.HEADER_ENCRYPTION_SECRET, this.encryptionSecret); + + return this; + } +} diff --git a/coreAuth/src/main/java/com/will/auth/config/SysSessionSetting.java b/coreAuth/src/main/java/com/will/auth/config/SysSessionSetting.java new file mode 100644 index 0000000..f30a05c --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/config/SysSessionSetting.java @@ -0,0 +1,12 @@ +package com.will.auth.config; + +public class SysSessionSetting { + public static final String HEADER_TOKEN = "token"; + public static final String HEADER_DEVICE_TYPE = "deviceType"; + public static final String HEADER_ENCRYPTION_SECRET = "encryptionSecret"; + public static final String HEADER_DATASOURCE = "dataSource"; + public static final String HEADER_USER = "user"; + public static final String HEADER_DATASOURCE_READ_ONLY = "dataSourceReadOnly"; + public static final String HEADER_IP_ADDRESS = "ipAddress"; + +} diff --git a/coreAuth/src/main/java/com/will/auth/config/UserContextSaTokenBridge.java b/coreAuth/src/main/java/com/will/auth/config/UserContextSaTokenBridge.java new file mode 100644 index 0000000..6d29812 --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/config/UserContextSaTokenBridge.java @@ -0,0 +1,23 @@ +package com.will.auth.config; + +import cn.dev33.satoken.stp.StpUtil; +import com.will.coreCommon.context.UserContext; +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; + +/** + * 将 Sa-Token 登录态回退绑定到 UserContext,避免 ThreadLocal 丢失后拿不到用户 ID。 + */ +@Component +public class UserContextSaTokenBridge { + + @PostConstruct + public void bindResolver() { + UserContext.setUserIdResolver(() -> { + if (!StpUtil.isLogin()) { + return null; + } + return StpUtil.getLoginIdAsString(); + }); + } +} diff --git a/coreAuth/src/main/java/com/will/auth/exception/GlobalExceptionHandler.java b/coreAuth/src/main/java/com/will/auth/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..d1d64b8 --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/exception/GlobalExceptionHandler.java @@ -0,0 +1,34 @@ +package com.will.auth.exception; + +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import jakarta.security.auth.message.AuthException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotLoginException.class) + public ResponseEntity> handleNotLogin(NotLoginException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ResponseMessageFactory.error("未登录")); + } + + @ExceptionHandler({NotPermissionException.class, NotRoleException.class, AuthException.class}) + public ResponseEntity> handleForbidden(Exception e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ResponseMessageFactory.error(e.getMessage() == null ? "没有权限" : e.getMessage())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleBadRequest(IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ResponseMessageFactory.error(e.getMessage())); + } +} diff --git a/coreAuth/src/main/java/com/will/auth/handler/SaTokenNotLoginHandler.java b/coreAuth/src/main/java/com/will/auth/handler/SaTokenNotLoginHandler.java new file mode 100644 index 0000000..3236de5 --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/handler/SaTokenNotLoginHandler.java @@ -0,0 +1,5 @@ +package com.will.auth.handler; + + +public class SaTokenNotLoginHandler { +} diff --git a/coreAuth/src/main/java/com/will/auth/interceptor/ApiPrefixGuardInterceptor.java b/coreAuth/src/main/java/com/will/auth/interceptor/ApiPrefixGuardInterceptor.java new file mode 100644 index 0000000..81211ad --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/interceptor/ApiPrefixGuardInterceptor.java @@ -0,0 +1,37 @@ +package com.will.auth.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 拒绝非 /api/** 的业务接口,避免 Controller 路径遗漏导致鉴权绕过。 + */ +@Slf4j +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ApiPrefixGuardInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String uri = request.getRequestURI(); + if (uri.startsWith("/api/") || isIgnored(uri)) { + return true; + } + log.warn("拒绝非 /api 前缀请求: {} {}", request.getMethod(), uri); + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return false; + } + + private boolean isIgnored(String uri) { + return uri.equals("/") + || uri.startsWith("/error") + || uri.startsWith("/actuator") + || uri.startsWith("/swagger") + || uri.startsWith("/v3/api-docs"); + } +} diff --git a/coreAuth/src/main/java/com/will/auth/interceptor/UserContextInterceptor.java b/coreAuth/src/main/java/com/will/auth/interceptor/UserContextInterceptor.java new file mode 100644 index 0000000..e350bc0 --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/interceptor/UserContextInterceptor.java @@ -0,0 +1,33 @@ +package com.will.auth.interceptor; + +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.stp.StpUtil; +import com.will.coreCommon.context.UserContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Slf4j +@Component +public class UserContextInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (StpUtil.isLogin()) { + String userId = StpUtil.getLoginIdAsString(); + UserContext.setCurrentUserId(userId); + log.debug("已设置当前用户ID: {}", userId); + return true; + } + + UserContext.clear(); + throw new NotLoginException("401 未登录", "user", "user"); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + UserContext.clear(); + } +} diff --git a/coreAuth/src/main/java/com/will/auth/service/AuthService.java b/coreAuth/src/main/java/com/will/auth/service/AuthService.java new file mode 100644 index 0000000..0c143a9 --- /dev/null +++ b/coreAuth/src/main/java/com/will/auth/service/AuthService.java @@ -0,0 +1,82 @@ +package com.will.auth.service; + +import cn.dev33.satoken.stp.StpUtil; +import com.will.coreCommon.context.UserContext; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreData.auth.AuthLoginSupport; +import com.will.coreData.entity.AuthUser; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final AuthLoginSupport authLoginSupport; + + public ResponseMessage login(String account, String password) { + if (account == null || account.isBlank() || password == null || password.isBlank()) { + return ResponseMessageFactory.error("账号和密码不能为空"); + } + + AuthUser user = authLoginSupport.findUserByAccount(account); + if (user == null) { + return ResponseMessageFactory.error("账号或密码错误"); + } + if (Boolean.FALSE.equals(user.getStatus())) { + return ResponseMessageFactory.error("账号已禁用"); + } + if (!authLoginSupport.verifyPassword(user.getId(), password)) { + return ResponseMessageFactory.error("账号或密码错误"); + } + + StpUtil.login(user.getId()); + UserContext.setCurrentUserId(user.getId()); + authLoginSupport.updateLastLoginTime(user); + + return ResponseMessageFactory.success(StpUtil.getTokenValue()); + } + + public ResponseMessage logout() { + if (StpUtil.isLogin()) { + StpUtil.logout(); + } + UserContext.clear(); + return ResponseMessageFactory.success(); + } + + public ResponseMessage> getAccessCodes() { + if (!StpUtil.isLogin()) { + throw new cn.dev33.satoken.exception.NotLoginException("未登录", "user", "user"); + } + return ResponseMessageFactory.success(StpUtil.getPermissionList()); + } + + public ResponseMessage> getUserInfo() { + if (!StpUtil.isLogin()) { + throw new cn.dev33.satoken.exception.NotLoginException("未登录", "user", "user"); + } + + String userId = StpUtil.getLoginIdAsString(); + AuthUser user = authLoginSupport.findUserById(userId); + if (user == null) { + return ResponseMessageFactory.error("用户不存在"); + } + + Map info = new HashMap<>(); + info.put("userId", user.getId()); + info.put("username", user.getAccount()); + info.put("realName", user.getUserName()); + info.put("avatar", ""); + info.put("desc", user.getUserName()); + info.put("homePath", "/dashboard/workspace"); + info.put("roles", StpUtil.getRoleList()); + info.put("token", StpUtil.getTokenValue()); + return ResponseMessageFactory.success(info); + } +} diff --git a/coreCommon/build.gradle b/coreCommon/build.gradle new file mode 100644 index 0000000..c5fc4ba --- /dev/null +++ b/coreCommon/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'java' +} \ No newline at end of file diff --git a/coreCommon/src/main/java/com/will/coreCommon/BaseController.java b/coreCommon/src/main/java/com/will/coreCommon/BaseController.java new file mode 100644 index 0000000..751280f --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/BaseController.java @@ -0,0 +1,4 @@ +package com.will.coreCommon; + +public class BaseController { +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/BaseListDTO.java b/coreCommon/src/main/java/com/will/coreCommon/BaseListDTO.java new file mode 100644 index 0000000..451c9ce --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/BaseListDTO.java @@ -0,0 +1,10 @@ +package com.will.coreCommon; + +import lombok.Data; + +import java.util.List; + +@Data +public class BaseListDTO { + List list; +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/BasePage.java b/coreCommon/src/main/java/com/will/coreCommon/BasePage.java new file mode 100644 index 0000000..5354cb5 --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/BasePage.java @@ -0,0 +1,18 @@ +package com.will.coreCommon; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class BasePage { + + public BasePage(List list, Long total) { + this.list = list; + this.total = total; + } + + List list = new ArrayList<>(); + long total; +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/BaseService.java b/coreCommon/src/main/java/com/will/coreCommon/BaseService.java new file mode 100644 index 0000000..d28624d --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/BaseService.java @@ -0,0 +1,5 @@ +package com.will.coreCommon; + +public class BaseService { + +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/auth/ApiPermissions.java b/coreCommon/src/main/java/com/will/coreCommon/auth/ApiPermissions.java new file mode 100644 index 0000000..d222483 --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/auth/ApiPermissions.java @@ -0,0 +1,88 @@ +package com.will.coreCommon.auth; + +import java.util.List; +import java.util.stream.Stream; + +/** + * API 权限码,与菜单 auth_menu.code / 前端 accessCodes 保持一致。 + */ +public final class ApiPermissions { + + private ApiPermissions() { + } + + public static final String DEPT_LIST = "sys:department:list"; + public static final String DEPT_TABLE_LIST = "sys:department:table:list"; + public static final String DEPT_SAVE = "sys:department:save"; + public static final String DEPT_DELETE = "sys:department:delete"; + public static final String DEPT_USERS = "sys:department:users"; + public static final String DEPT_TEST = "sys:department:test"; + + public static final String USER_LIST = "sys:user:list"; + public static final String USER_SAVE = "sys:user:save"; + public static final String USER_DELETE = "sys:user:delete"; + + public static final String ROLE_LIST = "sys:role:list"; + public static final String ROLE_SAVE = "sys:role:save"; + public static final String ROLE_DELETE = "sys:role:delete"; + public static final String ROLE_ASSIGN = "sys:role:assign"; + + public static final String MENU_LIST = "sys:menu:list"; + public static final String MENU_SAVE = "sys:menu:save"; + public static final String MENU_DELETE = "sys:menu:delete"; + + public static final String COMMON_TABLE_LIST = "sys:common:table:list"; + public static final String COMMON_TABLE_GET = "sys:common:table:get"; + + public static final String DATA_PERM_LIST = "sys:data-permission:list"; + public static final String DATA_PERM_SAVE = "sys:data-permission:save"; + + public static final String PERM_INFO_LIST = "sys:permission-info:list"; + + public static final String AGENT_LIST = "sys:agent:list"; + public static final String AGENT_CODE_GEN = "sys:agent:code:generate"; + public static final String CONFIG_LIST = "sys:config:list"; + public static final String SCHEDULE_JOB_LIST = "sys:schedule-job:list"; + public static final String SCHEDULE_JOB_SAVE = "sys:schedule-job:save"; + public static final String SCHEDULE_JOB_DELETE = "sys:schedule-job:delete"; + public static final String SCHEDULE_JOB_EXECUTE = "sys:schedule-job:execute"; + public static final String TASK_FUNCTION_LIST = "sys:task-function:list"; + public static final String TASK_FUNCTION_SAVE = "sys:task-function:save"; + public static final String TASK_FUNCTION_DELETE = "sys:task-function:delete"; + public static final String ROLE_SUPER = "super"; + + private static final List DEPT_PERMISSIONS = List.of( + DEPT_TEST, DEPT_LIST, DEPT_TABLE_LIST, DEPT_SAVE, DEPT_DELETE, DEPT_USERS + ); + + private static final List USER_PERMISSIONS = List.of( + USER_LIST, USER_SAVE, USER_DELETE + ); + + private static final List ROLE_PERMISSIONS = List.of( + ROLE_LIST, ROLE_SAVE, ROLE_DELETE, ROLE_ASSIGN + ); + + private static final List MENU_PERMISSIONS = List.of( + MENU_LIST, MENU_SAVE, MENU_DELETE + ); + + private static final List COMMON_PERMISSIONS = List.of( + COMMON_TABLE_LIST, COMMON_TABLE_GET + ); + + private static final List EXT_PERMISSIONS = List.of( + DATA_PERM_LIST, DATA_PERM_SAVE, PERM_INFO_LIST, + AGENT_LIST, AGENT_CODE_GEN, + CONFIG_LIST, + SCHEDULE_JOB_LIST, SCHEDULE_JOB_SAVE, SCHEDULE_JOB_DELETE, SCHEDULE_JOB_EXECUTE, + TASK_FUNCTION_LIST, TASK_FUNCTION_SAVE, TASK_FUNCTION_DELETE + ); + + public static final List ALL_API_PERMISSIONS = Stream.of( + DEPT_PERMISSIONS, USER_PERMISSIONS, ROLE_PERMISSIONS, MENU_PERMISSIONS, + COMMON_PERMISSIONS, EXT_PERMISSIONS + ).flatMap(List::stream).toList(); + + public static final String ROLE_ADMIN = "admin"; +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/auth/DataScope.java b/coreCommon/src/main/java/com/will/coreCommon/auth/DataScope.java new file mode 100644 index 0000000..26c28fb --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/auth/DataScope.java @@ -0,0 +1,19 @@ +package com.will.coreCommon.auth; + +/** + * 数据权限范围(后续在 Service 层按 scope 拼接查询条件)。 + */ +public enum DataScope { + + /** 全部数据 */ + ALL, + + /** 本部门 */ + DEPT, + + /** 本部门及下级 */ + DEPT_AND_CHILD, + + /** 仅本人 */ + SELF +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/auth/MenuTypes.java b/coreCommon/src/main/java/com/will/coreCommon/auth/MenuTypes.java new file mode 100644 index 0000000..354b8a2 --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/auth/MenuTypes.java @@ -0,0 +1,17 @@ +package com.will.coreCommon.auth; + +/** + * 菜单类型:DIR 目录、MENU 页面路由、PERM 按钮/API 权限点。 + */ +public final class MenuTypes { + + private MenuTypes() { + } + + public static final String DIR = "DIR"; + public static final String MENU = "MENU"; + public static final String PERM = "PERM"; + + /** 目录节点 code 前缀,不参与 API 权限校验 */ + public static final String ROUTE_CODE_PREFIX = "route:"; +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/context/DataAuthContext.java b/coreCommon/src/main/java/com/will/coreCommon/context/DataAuthContext.java new file mode 100644 index 0000000..063124d --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/context/DataAuthContext.java @@ -0,0 +1,25 @@ +package com.will.coreCommon.context; + +import com.will.coreCommon.auth.DataScope; +import lombok.Builder; +import lombok.Data; + +/** + * 当前请求的数据权限上下文(后续在 Service/DAO 层使用)。 + */ +@Data +@Builder +public class DataAuthContext { + + private String userId; + + private String departmentId; + + /** 当前角色数据范围,默认仅本人 */ + @Builder.Default + private DataScope scope = DataScope.SELF; + + public boolean isUnrestricted() { + return scope == DataScope.ALL; + } +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/context/UserContext.java b/coreCommon/src/main/java/com/will/coreCommon/context/UserContext.java new file mode 100644 index 0000000..5460c02 --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/context/UserContext.java @@ -0,0 +1,56 @@ +package com.will.coreCommon.context; + +/** + * 当前请求用户上下文(ThreadLocal)。 + * 可通过 {@link #setUserIdResolver} 注册回退解析(如从 Sa-Token 读取)。 + */ +public final class UserContext { + + private UserContext() { + } + + @FunctionalInterface + public interface UserIdResolver { + String resolve(); + } + + private static final ThreadLocal CURRENT_USER_ID = new ThreadLocal<>(); + private static volatile UserIdResolver userIdResolver; + + public static void setUserIdResolver(UserIdResolver resolver) { + userIdResolver = resolver; + } + + public static void setCurrentUserId(String userId) { + if (userId == null || userId.isBlank()) { + CURRENT_USER_ID.remove(); + } else { + CURRENT_USER_ID.set(userId); + } + } + + public static String getCurrentUserId() { + String userId = CURRENT_USER_ID.get(); + if (userId != null && !userId.isBlank()) { + return userId; + } + UserIdResolver resolver = userIdResolver; + if (resolver == null) { + return null; + } + try { + String resolved = resolver.resolve(); + if (resolved != null && !resolved.isBlank()) { + CURRENT_USER_ID.set(resolved); + return resolved; + } + } catch (Exception ignored) { + // 未登录或非 Web 线程时忽略 + } + return null; + } + + public static void clear() { + CURRENT_USER_ID.remove(); + } +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessage.java b/coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessage.java new file mode 100644 index 0000000..b7927ba --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessage.java @@ -0,0 +1,48 @@ +package com.will.coreCommon.general; + +import lombok.Getter; + +import java.util.List; + +public class ResponseMessage { + public static final String SUCCESS_CODE = "0"; + public static final String ERROR_CODE = "1"; + + public static final String SUCCESS_MSG= "success"; + public static final String ERROR_MSG= "error"; + + boolean notify = true; + @Getter + String code = ""; + @Getter + String message = ""; + + @Getter + T data; + + public ResponseMessage (String code, String msg){ + this.code = code; + this.message = msg; + } + + public ResponseMessage (String code, String msg, T data){ + this.code = code; + this.message = msg; + this.data = data; + } + + public ResponseMessage(T list) { + this.code = SUCCESS_CODE; + this.message = SUCCESS_MSG; + this.data = list; + } + + public boolean hasError(){ + return code.equalsIgnoreCase(ERROR_CODE); + } + + @Override + public String toString() { + return super.toString(); + } +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessageFactory.java b/coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessageFactory.java new file mode 100644 index 0000000..e258ce7 --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessageFactory.java @@ -0,0 +1,28 @@ +package com.will.coreCommon.general; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ResponseMessageFactory { + + public static ResponseMessage success() { + return new ResponseMessage<>(ResponseMessage.SUCCESS_CODE, ResponseMessage.SUCCESS_MSG); + } + public static ResponseMessage success(T data) { + return new ResponseMessage<>(ResponseMessage.SUCCESS_CODE, ResponseMessage.SUCCESS_MSG, data); + } + + public static ResponseMessage error(String message) { + return new ResponseMessage<>(ResponseMessage.ERROR_CODE, message); + } + public static ResponseMessage error(String message,T data) { + return new ResponseMessage<>(ResponseMessage.ERROR_CODE,message,data); + } + + public static ResponseMessage error(T data) { + return new ResponseMessage<>(ResponseMessage.ERROR_CODE, ResponseMessage.ERROR_MSG, data); + } + + +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/general/SystemConstants.java b/coreCommon/src/main/java/com/will/coreCommon/general/SystemConstants.java new file mode 100644 index 0000000..9c5a0dd --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/general/SystemConstants.java @@ -0,0 +1,13 @@ +package com.will.coreCommon.general; + +public class SystemConstants { + + public static final String SYSTEM_V_ADMIN_USER = "admin"; + public static final String SYSTEM_V_ADMIN_ROLE = "admin"; + public static final String SYSTEM_V_ADMIN_DEPARTMENT = "admin"; + + public static final String SYSTEM_SETTING_SYSTEM_INIT_FLAG= "SYSTEM_INIT_FLAG"; + + public static final String SYSTEM_V_Y= "Y"; + public static final String SYSTEM_V_N= "N"; +} diff --git a/coreCommon/src/main/java/com/will/coreCommon/util/Coder.java b/coreCommon/src/main/java/com/will/coreCommon/util/Coder.java new file mode 100644 index 0000000..96c10b3 --- /dev/null +++ b/coreCommon/src/main/java/com/will/coreCommon/util/Coder.java @@ -0,0 +1,82 @@ +package com.will.coreCommon.util; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import java.util.Base64; + +public class Coder { + + enum EncodeType { + none, + md5, + sha256 + } + + private static final Logger logger = LoggerFactory.getLogger(Coder.class); + + public static final String KEY_SHA = "SHA"; + public static final String KEY_SHA_256 = "SHA-256"; + public static final String KEY_MD5 = "MD5"; + + + public static String encryptMD5ToString(byte[] data) throws Exception { + return getPassword(data, KEY_MD5); + } + + public static String encryptSHA256toString(byte[] data) throws Exception { + return getPassword(data, KEY_SHA_256); + } + + private static String getPassword(byte[] data, String keySha256) throws NoSuchAlgorithmException { + MessageDigest md5 = MessageDigest.getInstance(keySha256); + md5.update(data); + byte[] b = md5.digest(); + int i; + StringBuilder buf = new StringBuilder(); + for (byte value : b) { + i = value; + if (i < 0) + i += 256; + if (i < 16) + buf.append("0"); + buf.append(Integer.toHexString(i)); + } + return buf.toString(); + } + + + static byte[] encryptMD5(byte[] data) throws Exception { + return encryptMD5ToString(data).getBytes(); + } + + static byte[] encryptSHA(byte[] data) throws Exception { + MessageDigest sha = MessageDigest.getInstance(KEY_SHA); + sha.update(data); + + return sha.digest(); + + } + + static byte[] encryptSHA256(byte[] data) throws Exception { + MessageDigest sha = MessageDigest.getInstance(KEY_SHA_256); + sha.update(data); + + return sha.digest(); + + } + + + public static String encoded256Password(byte[] bytes) throws Exception { + return encryptBASE64(encryptSHA(encryptSHA(encryptSHA256(bytes)))); + } + + static String encryptBASE64(byte[] key) throws Exception { + return new String(Base64.getEncoder().encode(key)); + } +} diff --git a/coreData/build.gradle b/coreData/build.gradle new file mode 100644 index 0000000..60d5627 --- /dev/null +++ b/coreData/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' +} + +dependencies { + + implementation project(':coreCommon') + + implementation("com.baomidou:mybatis-plus-jsqlparser:3.5.14") + implementation("com.mysql:mysql-connector-j:9.3.0") + + implementation("com.baomidou:mybatis-plus-spring-boot3-starter:3.5.14") +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/coreData/src/main/java/com/will/coreData/BaseDaoServiceImpl.java b/coreData/src/main/java/com/will/coreData/BaseDaoServiceImpl.java new file mode 100644 index 0000000..f8c2c4d --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/BaseDaoServiceImpl.java @@ -0,0 +1,25 @@ +package com.will.coreData; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreCommon.BasePage; + +import java.util.List; +import java.util.Map; + + +public class BaseDaoServiceImpl, T> extends ServiceImpl implements IBaseDaoService { + + public BasePage list(Long pageIndex, Long pageSize, List> conditions) { + Page page = new Page<>(pageIndex, pageSize); + QueryWrapper wrapper = new QueryWrapper<>(); + if (conditions != null) { + conditions.forEach(map -> wrapper.eq(map.get("key").toString(), map.get("value"))); + } + this.baseMapper.selectPage(page, wrapper); + + return new BasePage<>(page.getRecords(), page.getTotal()); + } +} diff --git a/coreData/src/main/java/com/will/coreData/BaseEntity.java b/coreData/src/main/java/com/will/coreData/BaseEntity.java new file mode 100644 index 0000000..fe834c5 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/BaseEntity.java @@ -0,0 +1,44 @@ +package com.will.coreData; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.Version; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +public class BaseEntity implements Serializable { + /** + * 创建时间 + */ + @TableField(value = "created",fill = FieldFill.INSERT) + private LocalDateTime created; + + /** + * 创建人 + */ + @TableField(value = "createdBy",fill = FieldFill.INSERT) + private String createdBy; + /** + * 修改时间: 修改时间 + */ + @TableField(value = "lastUpdated",fill = FieldFill.INSERT_UPDATE) + private LocalDateTime lastUpdated; + + /** + * 修改人: 修改人 + */ + @TableField( value = "lastUpdatedBy",fill = FieldFill.INSERT_UPDATE) + private String lastUpdatedBy; + + /** + * 版本号: 版本号 + */ + @Version + @TableField(value = "version") + private Integer version; + + +} diff --git a/coreData/src/main/java/com/will/coreData/BaseMapper.java b/coreData/src/main/java/com/will/coreData/BaseMapper.java new file mode 100644 index 0000000..2df7c83 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/BaseMapper.java @@ -0,0 +1,4 @@ +package com.will.coreData; + +public class BaseMapper { +} diff --git a/coreData/src/main/java/com/will/coreData/EntityMapperRegistry.java b/coreData/src/main/java/com/will/coreData/EntityMapperRegistry.java new file mode 100644 index 0000000..b98a23e --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/EntityMapperRegistry.java @@ -0,0 +1,48 @@ +package com.will.coreData; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthUser; +import com.will.coreData.mapper.AuthUserMapper; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Configuration +@RequiredArgsConstructor +public class EntityMapperRegistry { + + private static final Map> ENTITY_MAP = new ConcurrentHashMap<>(); + private static final Map MAPPER_BEAN_MAP = new ConcurrentHashMap<>(); + + private final ApplicationContext applicationContext; + + // 白名单:前端可用的表标识 + public static final Set ALLOWED_TABLE_CODES = Set.of("user"); + + @PostConstruct + public void init() { + register("user", AuthUser.class, AuthUserMapper.class); + } + + private void register(String tableCode, Class entityClass, Class mapperClass) { + ENTITY_MAP.put(tableCode, entityClass); + MAPPER_BEAN_MAP.put(tableCode, applicationContext.getBean(mapperClass)); + } + + public static Class getEntityClass(String tableCode) { + if (!ALLOWED_TABLE_CODES.contains(tableCode)) { + throw new IllegalArgumentException("不支持的表标识: " + tableCode); + } + return ENTITY_MAP.get(tableCode); + } + + @SuppressWarnings("unchecked") + public static BaseMapper getMapper(String tableCode) { + return (BaseMapper) MAPPER_BEAN_MAP.get(tableCode); + } +} diff --git a/coreData/src/main/java/com/will/coreData/IBaseDaoService.java b/coreData/src/main/java/com/will/coreData/IBaseDaoService.java new file mode 100644 index 0000000..02f42e4 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/IBaseDaoService.java @@ -0,0 +1,11 @@ +package com.will.coreData; + +import com.will.coreCommon.BasePage; + +import java.util.List; +import java.util.Map; + +public interface IBaseDaoService { + + BasePage list(Long pageIndex, Long pageSize, List> conditions); +} diff --git a/coreData/src/main/java/com/will/coreData/MyMetaObjectHandler.java b/coreData/src/main/java/com/will/coreData/MyMetaObjectHandler.java new file mode 100644 index 0000000..2cf68a8 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/MyMetaObjectHandler.java @@ -0,0 +1,38 @@ +package com.will.coreData; + + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.will.coreCommon.context.UserContext; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class MyMetaObjectHandler implements MetaObjectHandler { + @Override + public void insertFill(MetaObject metaObject) { + String loginCode = resolveLoginCode(); + this.strictInsertFill(metaObject, "version", Integer.class, 0); + this.strictInsertFill(metaObject, "created", LocalDateTime.class, LocalDateTime.now()); + this.strictInsertFill(metaObject, "createdBy", String.class, loginCode); + this.strictInsertFill(metaObject, "lastUpdatedBy", String.class, loginCode); + this.strictInsertFill(metaObject, "lastUpdated", LocalDateTime.class, LocalDateTime.now()); + } + + @Override + public void updateFill(MetaObject metaObject) { + String loginCode = resolveLoginCode(); + this.strictUpdateFill(metaObject, "lastUpdated", LocalDateTime.class, LocalDateTime.now()); + this.strictUpdateFill(metaObject, "lastUpdatedBy", String.class, loginCode); + } + + private String resolveLoginCode() { + try { + String userId = UserContext.getCurrentUserId(); + return userId == null ? "API" : userId; + } catch (Exception ignored) { + return "API"; + } + } +} diff --git a/coreData/src/main/java/com/will/coreData/MybatisPlusConfig.java b/coreData/src/main/java/com/will/coreData/MybatisPlusConfig.java new file mode 100644 index 0000000..28602c9 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/MybatisPlusConfig.java @@ -0,0 +1,25 @@ +package com.will.coreData; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MybatisPlusConfig { + /** + * 添加分页插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + + interceptor.addInnerInterceptor(new DataPermissionInterceptor()); + interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); //乐观锁插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加 + // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType + return interceptor; + } +} diff --git a/coreData/src/main/java/com/will/coreData/MybatisPlusUtils.java b/coreData/src/main/java/com/will/coreData/MybatisPlusUtils.java new file mode 100644 index 0000000..58c60b2 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/MybatisPlusUtils.java @@ -0,0 +1,67 @@ +package com.will.coreData; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class MybatisPlusUtils { + private MybatisPlusUtils() { + // Do not new me! + } + /** + * 传入mapper,paramList,源属性,新属性 得到 List<新属性> 用于关联表等 + * + * @param mapper 持久层操作类 + * @param param 查询参数 + * @param origin 源属性 + * @param target 新属性 + * @param PO + * @param T中的源属性 + * @param T中的新属性 + * @return java.util.Map + * @author + * @date 2021/5/4 0004 20:37 + */ + public static List getList(BaseMapper mapper, Supplier param, SFunction origin, SFunction target) { + return Optional.ofNullable(param).map(Supplier::get).map(p -> mapper.selectList(new LambdaQueryWrapper().eq(origin, p).select(target))).map(List::parallelStream).map(s -> s.map(target).collect(Collectors.toList())).orElseGet(Collections::emptyList); + } + /** + * 传入mapper,paramList,对应的属性 得到 Map<实体属性, 实体> 用于一对一 + * + * @param mapper 持久层操作类 + * @param paramList 查询参数 + * @param sFunction 条件 + * @param PO + * @param T中的属性 + * @return java.util.Map + * @author + * @date 2021/5/4 0004 20:37 + */ + public static Map getMapBy(BaseMapper mapper, List paramList, SFunction sFunction) { + return Optional.ofNullable(paramList).filter(CollectionUtils::isNotEmpty).map(p -> mapper.selectList(new LambdaQueryWrapper().in(sFunction, p))).map(List::parallelStream).map(s -> s.collect(Collectors.toMap(sFunction, Function.identity(), (a, b) -> b))).orElseGet(Collections::emptyMap); + } + /** + * 传入mapper,paramList,对应的属性 得到 Map<实体属性, List<实体>> 用于一对多 + * + * @param mapper 持久层操作类 + * @param paramList 查询参数 + * @param sFunction 条件 + * @param PO + * @param T中的属性 + * @return java.util.Map > + * @author + * @date 2021/5/4 0004 20:37 + */ + public static Map> groupBy(BaseMapper mapper, List paramList, SFunction sFunction) { + return Optional.ofNullable(paramList).filter(CollectionUtils::isNotEmpty).map(p -> mapper.selectList(new LambdaQueryWrapper().in(sFunction, p))).map(List::parallelStream).map(s -> s.collect(Collectors.groupingBy(sFunction))).orElseGet(Collections::emptyMap); + } +} diff --git a/coreData/src/main/java/com/will/coreData/MysqlConnectionStartupValidator.java b/coreData/src/main/java/com/will/coreData/MysqlConnectionStartupValidator.java new file mode 100644 index 0000000..31fd5cb --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/MysqlConnectionStartupValidator.java @@ -0,0 +1,40 @@ +package com.will.coreData; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContextException; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class MysqlConnectionStartupValidator implements ApplicationRunner { + + private final DataSource dataSource; + + @Override + public void run(ApplicationArguments args) throws Exception { + validateDatabaseConnection(); + } + + private void validateDatabaseConnection() throws Exception { + log.info("🔍 验证数据库连接..."); + try (Connection connection = dataSource.getConnection()) { + if (!connection.isValid(3)) { + throw new SQLException("数据库连接无效"); + } + log.info("✅ 数据库连接正常"); + } catch (Exception e) { + log.error("❌ 数据库连接失败: {}", e.getMessage()); + throw new ApplicationContextException("数据库不可用,请检查配置", e); + } + } + + +} \ No newline at end of file diff --git a/coreData/src/main/java/com/will/coreData/auth/AuthLoginSupport.java b/coreData/src/main/java/com/will/coreData/auth/AuthLoginSupport.java new file mode 100644 index 0000000..cc21df0 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/auth/AuthLoginSupport.java @@ -0,0 +1,63 @@ +package com.will.coreData.auth; + +import com.will.coreCommon.util.Coder; +import com.will.coreData.dao.IAuthUserPasswordService; +import com.will.coreData.dao.IAuthUserSaltService; +import com.will.coreData.dao.IAuthUserService; +import com.will.coreData.entity.AuthUser; +import com.will.coreData.entity.AuthUserPassword; +import com.will.coreData.entity.AuthUserSalt; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; + +/** + * 登录相关的数据访问封装,避免上层模块直接依赖 MyBatis-Plus API。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthLoginSupport { + + private final IAuthUserService authUserService; + private final IAuthUserPasswordService authUserPasswordService; + private final IAuthUserSaltService authUserSaltService; + + public AuthUser findUserByAccount(String account) { + return authUserService.findByAccount(account); + } + + public AuthUser findUserById(String userId) { + return authUserService.getById(userId); + } + + public void updateLastLoginTime(AuthUser user) { + user.setLastLoginTime(LocalDateTime.now()); + authUserService.updateById(user); + } + + public boolean verifyPassword(String userId, String rawPassword) { + AuthUserPassword passwordRecord = authUserPasswordService.findByUserId(userId); + if (passwordRecord == null || passwordRecord.getPassword() == null) { + return false; + } + + AuthUserSalt saltRecord = authUserSaltService.findByUserId(userId); + if (saltRecord != null && saltRecord.getSalt() != null) { + try { + String encoded = Coder.encoded256Password( + (rawPassword + saltRecord.getSalt()).getBytes(StandardCharsets.UTF_8)); + if (encoded.equals(passwordRecord.getPassword())) { + return true; + } + } catch (Exception e) { + log.warn("密码校验失败, userId={}", userId, e); + } + } + + return rawPassword.equals(passwordRecord.getPassword()); + } +} diff --git a/coreData/src/main/java/com/will/coreData/auth/AuthRbacSupport.java b/coreData/src/main/java/com/will/coreData/auth/AuthRbacSupport.java new file mode 100644 index 0000000..5b8c306 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/auth/AuthRbacSupport.java @@ -0,0 +1,97 @@ +package com.will.coreData.auth; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.will.coreCommon.auth.ApiPermissions; +import com.will.coreCommon.auth.MenuTypes; +import com.will.coreData.dao.IAuthMenuService; +import com.will.coreData.dao.IAuthRoleMenuService; +import com.will.coreData.dao.IAuthRoleService; +import com.will.coreData.dao.IAuthUserRoleService; +import com.will.coreData.entity.AuthMenu; +import com.will.coreData.entity.AuthRole; +import com.will.coreData.entity.AuthRoleMenu; +import com.will.coreData.entity.AuthUserRole; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthRbacSupport { + + private final IAuthUserRoleService authUserRoleService; + private final IAuthRoleService authRoleService; + private final IAuthRoleMenuService authRoleMenuService; + private final IAuthMenuService authMenuService; + + public List getRoleCodes(String userId) { + List roleIds = getRoleIds(userId); + if (roleIds.isEmpty()) { + return List.of(); + } + return authRoleService.listByIds(roleIds).stream() + .map(AuthRole::getCode) + .filter(StringUtils::hasText) + .distinct() + .toList(); + } + + public List getPermissionCodes(String userId) { + List roleCodes = getRoleCodes(userId); + if (roleCodes.isEmpty()) { + log.warn("用户 {} 未分配角色,无任何权限", userId); + return List.of(); + } + + List roleIds = getRoleIds(userId); + List menuIds = authRoleMenuService.list( + new LambdaQueryWrapper().in(AuthRoleMenu::getRoleId, roleIds)) + .stream() + .map(AuthRoleMenu::getMenuId) + .filter(StringUtils::hasText) + .distinct() + .toList(); + + if (menuIds.isEmpty()) { + return List.of(); + } + + return authMenuService.listByIds(menuIds).stream() + .filter(this::isGrantableMenu) + .map(AuthMenu::getCode) + .filter(StringUtils::hasText) + .distinct() + .toList(); + } + + private boolean isGrantableMenu(AuthMenu menu) { + if (menu == null || !StringUtils.hasText(menu.getMenuType())) { + return true; + } + return !MenuTypes.DIR.equals(menu.getMenuType()); + } + + public boolean isSuperAdmin(List roleCodes) { + return roleCodes.stream().anyMatch(code -> + ApiPermissions.ROLE_ADMIN.equalsIgnoreCase(code) + || ApiPermissions.ROLE_SUPER.equalsIgnoreCase(code)); + } + + private List getRoleIds(String userId) { + return authUserRoleService.list( + new LambdaQueryWrapper().eq(AuthUserRole::getUserId, userId)) + .stream() + .map(AuthUserRole::getRoleId) + .filter(StringUtils::hasText) + .distinct() + .toList(); + } + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAgentChatMessageService.java b/coreData/src/main/java/com/will/coreData/dao/IAgentChatMessageService.java new file mode 100644 index 0000000..80e5b9a --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAgentChatMessageService.java @@ -0,0 +1,7 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AgentChatMessage; + +public interface IAgentChatMessageService extends IService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAgentChatSessionService.java b/coreData/src/main/java/com/will/coreData/dao/IAgentChatSessionService.java new file mode 100644 index 0000000..e5d90a5 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAgentChatSessionService.java @@ -0,0 +1,7 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AgentChatSession; + +public interface IAgentChatSessionService extends IService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAgentConfigGroupService.java b/coreData/src/main/java/com/will/coreData/dao/IAgentConfigGroupService.java new file mode 100644 index 0000000..d6d30b8 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAgentConfigGroupService.java @@ -0,0 +1,7 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AgentConfigGroup; + +public interface IAgentConfigGroupService extends IService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAgentConfigItemService.java b/coreData/src/main/java/com/will/coreData/dao/IAgentConfigItemService.java new file mode 100644 index 0000000..de1447b --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAgentConfigItemService.java @@ -0,0 +1,7 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AgentConfigItem; + +public interface IAgentConfigItemService extends IService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAgentReportConfigService.java b/coreData/src/main/java/com/will/coreData/dao/IAgentReportConfigService.java new file mode 100644 index 0000000..192089d --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAgentReportConfigService.java @@ -0,0 +1,7 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AgentReportConfig; + +public interface IAgentReportConfigService extends IService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAgentReportJobConfigService.java b/coreData/src/main/java/com/will/coreData/dao/IAgentReportJobConfigService.java new file mode 100644 index 0000000..679f7d5 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAgentReportJobConfigService.java @@ -0,0 +1,7 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AgentReportJobConfig; + +public interface IAgentReportJobConfigService extends IService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthDataPermissionService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthDataPermissionService.java new file mode 100644 index 0000000..4f6d378 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthDataPermissionService.java @@ -0,0 +1,18 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthDataPermission; + +import java.util.List; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-07-08 + */ +public interface IAuthDataPermissionService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentService.java new file mode 100644 index 0000000..b78a845 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentService.java @@ -0,0 +1,14 @@ +package com.will.coreData.dao; +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthDepartment; +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-07-08 + */ +public interface IAuthDepartmentService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentUserService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentUserService.java new file mode 100644 index 0000000..e733d92 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentUserService.java @@ -0,0 +1,16 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthDepartmentUser; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-07-24 + */ +public interface IAuthDepartmentUserService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthMenuService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthMenuService.java new file mode 100644 index 0000000..a7c15af --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthMenuService.java @@ -0,0 +1,16 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthMenu; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthMenuService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthPermissionInfoService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthPermissionInfoService.java new file mode 100644 index 0000000..28fb556 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthPermissionInfoService.java @@ -0,0 +1,16 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthPermissionInfo; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-07-08 + */ +public interface IAuthPermissionInfoService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthRoleMenuService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthRoleMenuService.java new file mode 100644 index 0000000..a682185 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthRoleMenuService.java @@ -0,0 +1,16 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthRoleMenu; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthRoleMenuService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthRoleService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthRoleService.java new file mode 100644 index 0000000..8e2d063 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthRoleService.java @@ -0,0 +1,16 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthRole; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthRoleService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthRoleSettingService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthRoleSettingService.java new file mode 100644 index 0000000..4b40ec1 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthRoleSettingService.java @@ -0,0 +1,16 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthRoleSetting; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthRoleSettingService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthUserPasswordService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthUserPasswordService.java new file mode 100644 index 0000000..1281438 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthUserPasswordService.java @@ -0,0 +1,17 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthUserPassword; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthUserPasswordService extends IService { + + AuthUserPassword findByUserId(String userId); +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthUserRoleService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthUserRoleService.java new file mode 100644 index 0000000..4b06716 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthUserRoleService.java @@ -0,0 +1,16 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthUserRole; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthUserRoleService extends IService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthUserSaltService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthUserSaltService.java new file mode 100644 index 0000000..ae1e5a7 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthUserSaltService.java @@ -0,0 +1,17 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.entity.AuthUserSalt; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthUserSaltService extends IService { + + AuthUserSalt findByUserId(String userId); +} diff --git a/coreData/src/main/java/com/will/coreData/dao/IAuthUserService.java b/coreData/src/main/java/com/will/coreData/dao/IAuthUserService.java new file mode 100644 index 0000000..973042a --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/IAuthUserService.java @@ -0,0 +1,18 @@ +package com.will.coreData.dao; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.will.coreData.IBaseDaoService; +import com.will.coreData.entity.AuthUser; + +/** + *

+ * 服务类 + *

+ * + * @author will + * @since 2025-06-30 + */ +public interface IAuthUserService extends IService, IBaseDaoService { + + AuthUser findByAccount(String account); +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AgentChatMessageServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AgentChatMessageServiceImpl.java new file mode 100644 index 0000000..b82af3d --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AgentChatMessageServiceImpl.java @@ -0,0 +1,12 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAgentChatMessageService; +import com.will.coreData.entity.AgentChatMessage; +import com.will.coreData.mapper.AgentChatMessageMapper; +import org.springframework.stereotype.Service; + +@Service +public class AgentChatMessageServiceImpl extends ServiceImpl + implements IAgentChatMessageService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AgentChatSessionServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AgentChatSessionServiceImpl.java new file mode 100644 index 0000000..cd4ab8f --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AgentChatSessionServiceImpl.java @@ -0,0 +1,12 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAgentChatSessionService; +import com.will.coreData.entity.AgentChatSession; +import com.will.coreData.mapper.AgentChatSessionMapper; +import org.springframework.stereotype.Service; + +@Service +public class AgentChatSessionServiceImpl extends ServiceImpl + implements IAgentChatSessionService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigGroupServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigGroupServiceImpl.java new file mode 100644 index 0000000..af9b2bf --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigGroupServiceImpl.java @@ -0,0 +1,12 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAgentConfigGroupService; +import com.will.coreData.entity.AgentConfigGroup; +import com.will.coreData.mapper.AgentConfigGroupMapper; +import org.springframework.stereotype.Service; + +@Service +public class AgentConfigGroupServiceImpl extends ServiceImpl + implements IAgentConfigGroupService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigItemServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigItemServiceImpl.java new file mode 100644 index 0000000..35d479c --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigItemServiceImpl.java @@ -0,0 +1,12 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAgentConfigItemService; +import com.will.coreData.entity.AgentConfigItem; +import com.will.coreData.mapper.AgentConfigItemMapper; +import org.springframework.stereotype.Service; + +@Service +public class AgentConfigItemServiceImpl extends ServiceImpl + implements IAgentConfigItemService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AgentReportConfigServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AgentReportConfigServiceImpl.java new file mode 100644 index 0000000..884411d --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AgentReportConfigServiceImpl.java @@ -0,0 +1,12 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAgentReportConfigService; +import com.will.coreData.entity.AgentReportConfig; +import com.will.coreData.mapper.AgentReportConfigMapper; +import org.springframework.stereotype.Service; + +@Service +public class AgentReportConfigServiceImpl extends ServiceImpl + implements IAgentReportConfigService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AgentReportJobConfigServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AgentReportJobConfigServiceImpl.java new file mode 100644 index 0000000..d779190 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AgentReportJobConfigServiceImpl.java @@ -0,0 +1,12 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAgentReportJobConfigService; +import com.will.coreData.entity.AgentReportJobConfig; +import com.will.coreData.mapper.AgentReportJobConfigMapper; +import org.springframework.stereotype.Service; + +@Service +public class AgentReportJobConfigServiceImpl extends ServiceImpl + implements IAgentReportJobConfigService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthDataPermissionServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthDataPermissionServiceImpl.java new file mode 100644 index 0000000..f247b5c --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthDataPermissionServiceImpl.java @@ -0,0 +1,22 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthDataPermissionService; +import com.will.coreData.entity.AuthDataPermission; +import com.will.coreData.mapper.AuthDataPermissionMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-07-08 + */ +@Service +public class AuthDataPermissionServiceImpl extends ServiceImpl implements IAuthDataPermissionService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentServiceImpl.java new file mode 100644 index 0000000..a15537d --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentServiceImpl.java @@ -0,0 +1,21 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthDepartmentService; +import com.will.coreData.entity.AuthDepartment; +import com.will.coreData.mapper.AuthDepartmentMapper; +import org.springframework.stereotype.Service; + + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-07-08 + */ +@Service +public class AuthDepartmentServiceImpl extends ServiceImpl implements IAuthDepartmentService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentUserServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentUserServiceImpl.java new file mode 100644 index 0000000..f451e20 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentUserServiceImpl.java @@ -0,0 +1,13 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthDepartmentUserService; +import com.will.coreData.entity.AuthDepartmentUser; +import com.will.coreData.mapper.AuthDepartmentUserMapper; +import org.springframework.stereotype.Service; + +@Service +public class AuthDepartmentUserServiceImpl + extends ServiceImpl + implements IAuthDepartmentUserService { +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthMenuServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthMenuServiceImpl.java new file mode 100644 index 0000000..2056906 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthMenuServiceImpl.java @@ -0,0 +1,20 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthMenuService; +import com.will.coreData.entity.AuthMenu; +import com.will.coreData.mapper.AuthMenuMapper; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Service +public class AuthMenuServiceImpl extends ServiceImpl implements IAuthMenuService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthPermissionInfoServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthPermissionInfoServiceImpl.java new file mode 100644 index 0000000..f24a5fd --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthPermissionInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthPermissionInfoService; +import com.will.coreData.entity.AuthPermissionInfo; +import com.will.coreData.mapper.AuthPermissionInfoMapper; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-07-08 + */ +@Service +public class AuthPermissionInfoServiceImpl extends ServiceImpl implements IAuthPermissionInfoService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleMenuServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleMenuServiceImpl.java new file mode 100644 index 0000000..e03f033 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleMenuServiceImpl.java @@ -0,0 +1,28 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthRoleMenuService; +import com.will.coreData.entity.AuthRoleMenu; +import com.will.coreData.mapper.AuthRoleMenuMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Service +public class AuthRoleMenuServiceImpl extends ServiceImpl implements IAuthRoleMenuService { + + public List getMenuByRoleId(String roleId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AuthRoleMenu::getRoleId, roleId); + return list(queryWrapper); + } +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleServiceImpl.java new file mode 100644 index 0000000..ee47191 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleServiceImpl.java @@ -0,0 +1,23 @@ +package com.will.coreData.dao.impl; + + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthRoleService; +import com.will.coreData.entity.AuthRole; +import com.will.coreData.mapper.AuthRoleMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Service +public class AuthRoleServiceImpl extends ServiceImpl implements IAuthRoleService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleSettingServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleSettingServiceImpl.java new file mode 100644 index 0000000..da319f4 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleSettingServiceImpl.java @@ -0,0 +1,20 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthRoleSettingService; +import com.will.coreData.entity.AuthRoleSetting; +import com.will.coreData.mapper.AuthRoleSettingMapper; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Service +public class AuthRoleSettingServiceImpl extends ServiceImpl implements IAuthRoleSettingService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserPasswordServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserPasswordServiceImpl.java new file mode 100644 index 0000000..5a1408a --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserPasswordServiceImpl.java @@ -0,0 +1,25 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthUserPasswordService; +import com.will.coreData.entity.AuthUserPassword; +import com.will.coreData.mapper.AuthUserPasswordMapper; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Service +public class AuthUserPasswordServiceImpl extends ServiceImpl implements IAuthUserPasswordService { + + @Override + public AuthUserPassword findByUserId(String userId) { + return getOne(new LambdaQueryWrapper().eq(AuthUserPassword::getUserId, userId)); + } +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserRoleServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserRoleServiceImpl.java new file mode 100644 index 0000000..a49adf1 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserRoleServiceImpl.java @@ -0,0 +1,20 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthUserRoleService; +import com.will.coreData.entity.AuthUserRole; +import com.will.coreData.mapper.AuthUserRoleMapper; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Service +public class AuthUserRoleServiceImpl extends ServiceImpl implements IAuthUserRoleService { + +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserSaltServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserSaltServiceImpl.java new file mode 100644 index 0000000..9f982f2 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserSaltServiceImpl.java @@ -0,0 +1,25 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.will.coreData.dao.IAuthUserSaltService; +import com.will.coreData.entity.AuthUserSalt; +import com.will.coreData.mapper.AuthUserSaltMapper; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Service +public class AuthUserSaltServiceImpl extends ServiceImpl implements IAuthUserSaltService { + + @Override + public AuthUserSalt findByUserId(String userId) { + return getOne(new LambdaQueryWrapper().eq(AuthUserSalt::getUserId, userId)); + } +} diff --git a/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserServiceImpl.java b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserServiceImpl.java new file mode 100644 index 0000000..826f6ce --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/dao/impl/AuthUserServiceImpl.java @@ -0,0 +1,17 @@ +package com.will.coreData.dao.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.will.coreData.BaseDaoServiceImpl; +import com.will.coreData.dao.IAuthUserService; +import com.will.coreData.entity.AuthUser; +import com.will.coreData.mapper.AuthUserMapper; +import org.springframework.stereotype.Service; + +@Service +public class AuthUserServiceImpl extends BaseDaoServiceImpl implements IAuthUserService { + + @Override + public AuthUser findByAccount(String account) { + return getOne(new LambdaQueryWrapper().eq(AuthUser::getAccount, account)); + } +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AgentChatMessage.java b/coreData/src/main/java/com/will/coreData/entity/AgentChatMessage.java new file mode 100644 index 0000000..98316ea --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AgentChatMessage.java @@ -0,0 +1,37 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("agent_chat_message") +public class AgentChatMessage extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("sessionId") + private String sessionId; + + @TableField("userId") + private String userId; + + @TableField("role") + private String role; + + @TableField("content") + private String content; + + @TableField("sortOrder") + private Integer sortOrder; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AgentChatSession.java b/coreData/src/main/java/com/will/coreData/entity/AgentChatSession.java new file mode 100644 index 0000000..315cd66 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AgentChatSession.java @@ -0,0 +1,37 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("agent_chat_session") +public class AgentChatSession extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("userId") + private String userId; + + @TableField("title") + private String title; + + @TableField("scope") + private String scope; + + @TableField("backendSessionId") + private String backendSessionId; + + @TableField("deleted") + private Boolean deleted; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AgentConfigGroup.java b/coreData/src/main/java/com/will/coreData/entity/AgentConfigGroup.java new file mode 100644 index 0000000..6ec6874 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AgentConfigGroup.java @@ -0,0 +1,37 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("agent_config_group") +public class AgentConfigGroup extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("code") + private String code; + + @TableField("name") + private String name; + + @TableField("description") + private String description; + + @TableField("enabled") + private Boolean enabled; + + @TableField("sortOrder") + private Integer sortOrder; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AgentConfigItem.java b/coreData/src/main/java/com/will/coreData/entity/AgentConfigItem.java new file mode 100644 index 0000000..23582a3 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AgentConfigItem.java @@ -0,0 +1,40 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("agent_config_item") +public class AgentConfigItem extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("groupId") + private String groupId; + + @TableField("code") + private String code; + + @TableField("name") + private String name; + + @TableField("description") + private String description; + + @TableField("enabled") + private Boolean enabled; + + @TableField("sortOrder") + private Integer sortOrder; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AgentReportConfig.java b/coreData/src/main/java/com/will/coreData/entity/AgentReportConfig.java new file mode 100644 index 0000000..215a1fc --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AgentReportConfig.java @@ -0,0 +1,73 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("agent_report_config") +public class AgentReportConfig extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("enabled") + private Boolean enabled; + + @TableField("robotEnabled") + private Boolean robotEnabled; + + @TableField("robotType") + private String robotType; + + @TableField("webhookUrl") + private String webhookUrl; + + + @TableField("mailTo") + private String mailTo; + + @TableField("mailSubject") + private String mailSubject; +@TableField("dailyEffortEnabled") + private Boolean dailyEffortEnabled; + + @TableField("localSchedulerEnabled") + private Boolean localSchedulerEnabled; + + @TableField("cron") + private String cron; + + @TableField("zone") + private String zone; + + @TableField("daysAgo") + private Integer daysAgo; + + @TableField("groupName") + private String groupName; + + @TableField("xxlJobEnabled") + private Boolean xxlJobEnabled; + + @TableField("xxlAdminAddresses") + private String xxlAdminAddresses; + + @TableField("xxlJobGroupId") + private Integer xxlJobGroupId; + + @TableField("xxlAppname") + private String xxlAppname; + + @TableField("xxlExecutorPort") + private Integer xxlExecutorPort; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AgentReportJobConfig.java b/coreData/src/main/java/com/will/coreData/entity/AgentReportJobConfig.java new file mode 100644 index 0000000..3185542 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AgentReportJobConfig.java @@ -0,0 +1,64 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("agent_report_job_config") +public class AgentReportJobConfig extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("name") + private String name; + + @TableField("enabled") + private Boolean enabled; + + @TableField("jobType") + private String jobType; + + @TableField("cron") + private String cron; + + @TableField("zone") + private String zone; + + @TableField("daysAgo") + private Integer daysAgo; + + @TableField("groupName") + private String groupName; + + @TableField("robotType") + private String robotType; + + @TableField("webhookUrl") + private String webhookUrl; + + + @TableField("mailTo") + private String mailTo; + + @TableField("mailSubject") + private String mailSubject; +@TableField("xxlJobId") + private Integer xxlJobId; + + @TableField("localSchedulerEnabled") + private Boolean localSchedulerEnabled; + + @TableField("remark") + private String remark; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthDataPermission.java b/coreData/src/main/java/com/will/coreData/entity/AuthDataPermission.java new file mode 100644 index 0000000..664696b --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthDataPermission.java @@ -0,0 +1,47 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; + +import java.io.Serial; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-07-08 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_data_permission") +public class AuthDataPermission extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("permissionId") + private String permissionId; + + @TableField("roleId") + private String roleId; + + @TableField("userId") + private String userId; + + @TableField("readPermission") + private String readPermission; + + @TableField("editPermission") + private String editPermission; + + @TableField("deletePermission") + private String deletePermission; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthDepartment.java b/coreData/src/main/java/com/will/coreData/entity/AuthDepartment.java new file mode 100644 index 0000000..49c4d4c --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthDepartment.java @@ -0,0 +1,52 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-07-08 + */ +@Getter +@Setter +@ToString +@TableName("auth_department") +public class AuthDepartment extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + /** + * 编码 + */ + @TableField("code") + private String code; + + /** + * 服务名称 + */ + @TableField("name") + private String name; + + /** + * 父节点 + */ + @TableField("parentId") + private String parentId; + + @TableField("status") + private Integer status; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthDepartmentUser.java b/coreData/src/main/java/com/will/coreData/entity/AuthDepartmentUser.java new file mode 100644 index 0000000..ce6e4e3 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthDepartmentUser.java @@ -0,0 +1,36 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; + +import java.io.Serial; +import java.io.Serializable; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-07-24 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_department_user") +public class AuthDepartmentUser extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("departmentId") + private String departmentId; + + @TableField("userId") + private String userId; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthMenu.java b/coreData/src/main/java/com/will/coreData/entity/AuthMenu.java new file mode 100644 index 0000000..2cda113 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthMenu.java @@ -0,0 +1,78 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import com.will.coreData.BaseEntity; +import lombok.*; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_menu") +public class AuthMenu extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + /** + * 编码 + */ + @TableField("code") + private String code; + + /** + * 服务名称 + */ + @TableField("name") + private String name; + + /** + * 父节点 + */ + @TableField("parentId") + private String parentId; + + /** DIR 目录 / MENU 页面 / PERM 权限点 */ + @TableField("menuType") + private String menuType; + + @TableField("routePath") + private String routePath; + + @TableField("routeName") + private String routeName; + + @TableField("component") + private String component; + + @TableField("redirect") + private String redirect; + + @TableField("icon") + private String icon; + + @TableField("sortOrder") + private Integer sortOrder; + + /** 前端 meta.title(i18n key) */ + @TableField("title") + private String title; + +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthPermissionInfo.java b/coreData/src/main/java/com/will/coreData/entity/AuthPermissionInfo.java new file mode 100644 index 0000000..1f18fb6 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthPermissionInfo.java @@ -0,0 +1,37 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import com.will.coreData.BaseEntity; +import lombok.*; + +import java.io.Serial; +import java.io.Serializable; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-07-08 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_permission_info") +public class AuthPermissionInfo extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("code") + private String code; + + @TableField("name") + private String name; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthRole.java b/coreData/src/main/java/com/will/coreData/entity/AuthRole.java new file mode 100644 index 0000000..5a9dd22 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthRole.java @@ -0,0 +1,42 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_role") +public class AuthRole extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("code") + private String code; + + @TableField("name") + private String name; + + @TableField("departmentId") + private String departmentId; + +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthRoleMenu.java b/coreData/src/main/java/com/will/coreData/entity/AuthRoleMenu.java new file mode 100644 index 0000000..eb2f5cf --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthRoleMenu.java @@ -0,0 +1,40 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@Data +@TableName("auth_role_menu") +public class AuthRoleMenu implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("roleId") + private String roleId; + + @TableField("menuId") + private String menuId; + +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthRoleSetting.java b/coreData/src/main/java/com/will/coreData/entity/AuthRoleSetting.java new file mode 100644 index 0000000..382e975 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthRoleSetting.java @@ -0,0 +1,81 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_role_setting") +public class AuthRoleSetting extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("roleId") + private String roleId; + + /** + * 密码过期时间 + */ + @TableField("passwordExpire") + private Integer passwordExpire; + + /** + * 历史密码记录次数 + */ + @TableField("passwordHistory") + private Integer passwordHistory; + + /** + * 密码最小长度 + */ + @TableField("passwordMinimumLength") + private Integer passwordMinimumLength; + + /** + * 密码生存周期 + */ + @TableField("passwordLifetime") + private Integer passwordLifetime; + + /** + * 可尝试登录次数 + */ + @TableField("loginAttempts") + private Integer loginAttempts; + + /** + * 登录锁定周期 + */ + @TableField("loginLockoutPeriod") + private Integer loginLockoutPeriod; + + /** + * Session过期时间 + */ + @TableField("sessionTimeout") + private Integer sessionTimeout; + + + @TableField("passwordComplexity") + private String passwordComplexity; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthUser.java b/coreData/src/main/java/com/will/coreData/entity/AuthUser.java new file mode 100644 index 0000000..8c0cc4b --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthUser.java @@ -0,0 +1,55 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_user") +public class AuthUser extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("account") + private String account; + + @TableField("userName") + private String userName; + + @TableField("email") + private String email; + + @TableField("phoneNumber") + private Integer phoneNumber; + /** + * 0禁用;1启用 + */ + @TableField("status") + private Boolean status; + /** + * 最后一次登录时间 + */ + @TableField("lastLoginTime") + private LocalDateTime lastLoginTime; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthUserPassword.java b/coreData/src/main/java/com/will/coreData/entity/AuthUserPassword.java new file mode 100644 index 0000000..abef3eb --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthUserPassword.java @@ -0,0 +1,39 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_user_password") +public class AuthUserPassword extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("userId") + private String userId; + + @TableField("password") + private String password; + +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthUserRole.java b/coreData/src/main/java/com/will/coreData/entity/AuthUserRole.java new file mode 100644 index 0000000..5061695 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthUserRole.java @@ -0,0 +1,50 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_user_role") +public class AuthUserRole extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + /** + * 用户ID + */ + @TableField("userId") + private String userId; + + /** + * 角色ID + */ + @TableField("roleId") + private String roleId; + + /** + * 部门ID + */ + @TableField("departmentId") + private String departmentId; +} diff --git a/coreData/src/main/java/com/will/coreData/entity/AuthUserSalt.java b/coreData/src/main/java/com/will/coreData/entity/AuthUserSalt.java new file mode 100644 index 0000000..1bd053e --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/entity/AuthUserSalt.java @@ -0,0 +1,39 @@ +package com.will.coreData.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.will.coreData.BaseEntity; +import lombok.*; +import org.apache.ibatis.annotations.Mapper; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author will + * @since 2025-06-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("auth_user_salt") +public class AuthUserSalt extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + + @TableField("userId") + private String userId; + + @TableField("salt") + private String salt; + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AgentChatMessageMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AgentChatMessageMapper.java new file mode 100644 index 0000000..97b439e --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AgentChatMessageMapper.java @@ -0,0 +1,9 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AgentChatMessage; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AgentChatMessageMapper extends BaseMapper { +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AgentChatSessionMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AgentChatSessionMapper.java new file mode 100644 index 0000000..a4948f4 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AgentChatSessionMapper.java @@ -0,0 +1,9 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AgentChatSession; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AgentChatSessionMapper extends BaseMapper { +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AgentConfigGroupMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AgentConfigGroupMapper.java new file mode 100644 index 0000000..34391fe --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AgentConfigGroupMapper.java @@ -0,0 +1,9 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AgentConfigGroup; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AgentConfigGroupMapper extends BaseMapper { +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AgentConfigItemMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AgentConfigItemMapper.java new file mode 100644 index 0000000..00d26f1 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AgentConfigItemMapper.java @@ -0,0 +1,9 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AgentConfigItem; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AgentConfigItemMapper extends BaseMapper { +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AgentReportConfigMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AgentReportConfigMapper.java new file mode 100644 index 0000000..40d5638 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AgentReportConfigMapper.java @@ -0,0 +1,9 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AgentReportConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AgentReportConfigMapper extends BaseMapper { +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AgentReportJobConfigMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AgentReportJobConfigMapper.java new file mode 100644 index 0000000..2e73fd2 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AgentReportJobConfigMapper.java @@ -0,0 +1,9 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AgentReportJobConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AgentReportJobConfigMapper extends BaseMapper { +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthDataPermissionMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthDataPermissionMapper.java new file mode 100644 index 0000000..58e61b0 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthDataPermissionMapper.java @@ -0,0 +1,22 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthDataPermission; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-07-08 + */ +@Mapper +public interface AuthDataPermissionMapper extends BaseMapper { + @Select("select * from auth_data_permission where roleId = #{roleId}") + List getPermissionByRoleId(String roleId); +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentMapper.java new file mode 100644 index 0000000..b78bc63 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthDepartment; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-07-08 + */ +@Mapper +public interface AuthDepartmentMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentUserMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentUserMapper.java new file mode 100644 index 0000000..082c8db --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentUserMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthDepartmentUser; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-07-24 + */ +@Mapper +public interface AuthDepartmentUserMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthMenuMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthMenuMapper.java new file mode 100644 index 0000000..6f061d7 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthMenuMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthMenu; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthMenuMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthPermissionInfoMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthPermissionInfoMapper.java new file mode 100644 index 0000000..dd6150c --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthPermissionInfoMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthPermissionInfo; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-07-08 + */ +@Mapper +public interface AuthPermissionInfoMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthRoleMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthRoleMapper.java new file mode 100644 index 0000000..9df0181 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthRoleMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthRole; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthRoleMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthRoleMenuMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthRoleMenuMapper.java new file mode 100644 index 0000000..ee46acf --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthRoleMenuMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthRoleMenu; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthRoleMenuMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthRoleSettingMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthRoleSettingMapper.java new file mode 100644 index 0000000..6728c20 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthRoleSettingMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthRoleSetting; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthRoleSettingMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthUserMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthUserMapper.java new file mode 100644 index 0000000..f2f8cab --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthUserMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthUser; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthUserMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthUserPasswordMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthUserPasswordMapper.java new file mode 100644 index 0000000..7bdfce3 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthUserPasswordMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthUserPassword; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthUserPasswordMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthUserRoleMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthUserRoleMapper.java new file mode 100644 index 0000000..2763d2f --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthUserRoleMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthUserRole; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthUserRoleMapper extends BaseMapper { + +} diff --git a/coreData/src/main/java/com/will/coreData/mapper/AuthUserSaltMapper.java b/coreData/src/main/java/com/will/coreData/mapper/AuthUserSaltMapper.java new file mode 100644 index 0000000..34bf742 --- /dev/null +++ b/coreData/src/main/java/com/will/coreData/mapper/AuthUserSaltMapper.java @@ -0,0 +1,18 @@ +package com.will.coreData.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.will.coreData.entity.AuthUserSalt; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author will + * @since 2025-06-30 + */ +@Mapper +public interface AuthUserSaltMapper extends BaseMapper { + +} diff --git a/coreJob/build.gradle b/coreJob/build.gradle new file mode 100644 index 0000000..314c7c6 --- /dev/null +++ b/coreJob/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(':coreCommon') + implementation project(':coreService') + implementation project(':businessJob') + + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'com.xuxueli:xxl-job-core:3.4.2' +} diff --git a/coreJob/src/main/java/com/will/coreJob/framework/config/XxlJobConfiguration.java b/coreJob/src/main/java/com/will/coreJob/framework/config/XxlJobConfiguration.java new file mode 100644 index 0000000..6e953e6 --- /dev/null +++ b/coreJob/src/main/java/com/will/coreJob/framework/config/XxlJobConfiguration.java @@ -0,0 +1,69 @@ +package com.will.coreJob.framework.config; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import com.will.coreService.dto.AgentReportConfigDTO; +import com.will.coreService.service.AgentReportConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +@Slf4j +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties(ZentaoReportIntegrationProperties.class) +@ConditionalOnProperty(prefix = "will.agent.zentao.report.xxl-job", name = "enabled", havingValue = "true") +public class XxlJobConfiguration { + + private final ZentaoReportIntegrationProperties properties; + private final ObjectProvider reportConfigService; + + @Bean + public XxlJobSpringExecutor xxlJobExecutor() { + ZentaoReportIntegrationProperties.XxlJob xxlJob = properties.getXxlJob(); + AgentReportConfigDTO dbConfig = loadDbConfig(); + XxlJobSpringExecutor executor = new XxlJobSpringExecutor(); + executor.setAdminAddresses(StringUtils.hasText(value(dbConfig == null ? null : dbConfig.getXxlAdminAddresses())) + ? dbConfig.getXxlAdminAddresses().trim() + : trimToNull(xxlJob.getAdminAddresses())); + executor.setAppname(StringUtils.hasText(value(dbConfig == null ? null : dbConfig.getXxlAppname())) + ? dbConfig.getXxlAppname().trim() + : trimToNull(xxlJob.getAppname())); + executor.setAddress(trimToNull(xxlJob.getAddress())); + executor.setIp(trimToNull(xxlJob.getIp())); + executor.setPort(dbConfig != null && dbConfig.getXxlExecutorPort() != null + ? dbConfig.getXxlExecutorPort() + : xxlJob.getPort()); + executor.setAccessToken(trimToNull(xxlJob.getAccessToken())); + executor.setLogPath(trimToNull(xxlJob.getLogPath())); + executor.setLogRetentionDays(xxlJob.getLogRetentionDays()); + if (!StringUtils.hasText(xxlJob.getAdminAddresses())) { + log.warn("[XXL-JOB] executor enabled but adminAddresses is empty"); + } + log.info("[XXL-JOB] executor configured, appname={}, adminAddresses={}, port={}", + xxlJob.getAppname(), xxlJob.getAdminAddresses(), xxlJob.getPort()); + return executor; + } + + private AgentReportConfigDTO loadDbConfig() { + try { + AgentReportConfigService service = reportConfigService.getIfAvailable(); + return service == null ? null : service.getConfig(); + } catch (Exception ex) { + log.warn("[XXL-JOB] load db config failed, fallback to application.yml: {}", ex.getMessage()); + return null; + } + } + + private String value(String value) { + return value == null ? "" : value; + } + + private String trimToNull(String value) { + return StringUtils.hasText(value) ? value.trim() : null; + } +} diff --git a/coreJob/src/main/java/com/will/coreJob/framework/config/ZentaoReportIntegrationProperties.java b/coreJob/src/main/java/com/will/coreJob/framework/config/ZentaoReportIntegrationProperties.java new file mode 100644 index 0000000..800b71f --- /dev/null +++ b/coreJob/src/main/java/com/will/coreJob/framework/config/ZentaoReportIntegrationProperties.java @@ -0,0 +1,27 @@ +package com.will.coreJob.framework.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "will.agent.zentao.report") +public class ZentaoReportIntegrationProperties { + + private XxlJob xxlJob = new XxlJob(); + + @Data + public static class XxlJob { + private boolean enabled = false; + private String adminAddresses; + private String adminUsername = ""; + private String adminPassword = ""; + private String accessToken; + private Integer jobGroupId; + private String appname = "will-system-agent"; + private String address; + private String ip; + private int port = 9999; + private String logPath = "logs/xxl-job/jobhandler"; + private int logRetentionDays = 30; + } +} diff --git a/coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobAdminClient.java b/coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobAdminClient.java new file mode 100644 index 0000000..dff0db9 --- /dev/null +++ b/coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobAdminClient.java @@ -0,0 +1,251 @@ +package com.will.coreJob.framework.xxl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.will.coreJob.framework.config.ZentaoReportIntegrationProperties; +import com.will.coreService.dto.AgentReportConfigDTO; +import com.will.coreService.dto.AgentReportJobConfigDTO; +import com.will.coreService.service.AgentReportConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class XxlJobAdminClient { + + private static final String DISPATCH_HANDLER = "agentReportDispatchJob"; + + private final ZentaoReportIntegrationProperties properties; + private final AgentReportConfigService reportConfigService; + private final ObjectMapper objectMapper; + + public Integer saveJob(AgentReportJobConfigDTO jobConfig) { + AgentReportConfigDTO reportConfig = reportConfigService.getConfig(); + if (!properties.getXxlJob().isEnabled()) { + log.info("[XXL-JOB][Sync] skipped because XXL-JOB is disabled, jobId={}", jobConfig.getId()); + return jobConfig.getXxlJobId(); + } + + ClientSession session = login(reportConfig); + int jobGroup = resolveExecutorGroup(reportConfig); + Integer xxlJobId = jobConfig.getXxlJobId(); + if (xxlJobId == null) { + xxlJobId = addJob(session, jobGroup, jobConfig); + } else { + updateJob(session, jobGroup, jobConfig, xxlJobId); + } + if (xxlJobId == null || xxlJobId <= 0) { + throw new IllegalStateException("XXL-JOB job id is invalid after sync: " + xxlJobId); + } + log.info("[XXL-JOB][Sync] job synced, configId={}, xxlJobId={}, jobGroup={}, enabled={}", + jobConfig.getId(), xxlJobId, jobGroup, jobConfig.getEnabled()); + + if (Boolean.TRUE.equals(jobConfig.getEnabled())) { + startJob(session, xxlJobId); + } else { + stopJob(session, xxlJobId); + } + return xxlJobId; + } + + public void removeJob(AgentReportJobConfigDTO jobConfig) { + if (jobConfig.getXxlJobId() == null) { + return; + } + AgentReportConfigDTO reportConfig = reportConfigService.getConfig(); + if (!properties.getXxlJob().isEnabled()) { + return; + } + ClientSession session = login(reportConfig); + postForm(session, "/jobinfo/delete", Map.of("ids[]", jobConfig.getXxlJobId().toString())); + } + + private Integer addJob(ClientSession session, int jobGroup, AgentReportJobConfigDTO jobConfig) { + JsonNode response = postForm(session, "/jobinfo/insert", jobInfoForm(jobGroup, jobConfig, null)); + log.info("[XXL-JOB][Sync] add job response, configId={}, response={}", jobConfig.getId(), response); + JsonNode data = response.path("data"); + if (data.isInt()) { + return data.asInt(); + } + if (data.isTextual() && StringUtils.hasText(data.asText())) { + return Integer.parseInt(data.asText()); + } + JsonNode content = response.path("content"); + if (content.isInt()) { + return content.asInt(); + } + if (content.isTextual() && StringUtils.hasText(content.asText())) { + return Integer.parseInt(content.asText()); + } + throw new IllegalStateException("XXL-JOB job creation did not return job id: " + response); + } + + private void updateJob(ClientSession session, int jobGroup, AgentReportJobConfigDTO jobConfig, Integer xxlJobId) { + postForm(session, "/jobinfo/update", jobInfoForm(jobGroup, jobConfig, xxlJobId)); + } + + private void startJob(ClientSession session, Integer xxlJobId) { + postForm(session, "/jobinfo/start", Map.of("ids[]", xxlJobId.toString())); + } + + private void stopJob(ClientSession session, Integer xxlJobId) { + postForm(session, "/jobinfo/stop", Map.of("ids[]", xxlJobId.toString())); + } + + private int resolveExecutorGroup(AgentReportConfigDTO reportConfig) { + if (reportConfig.getXxlJobGroupId() != null && reportConfig.getXxlJobGroupId() > 0) { + return reportConfig.getXxlJobGroupId(); + } + if (properties.getXxlJob().getJobGroupId() != null && properties.getXxlJob().getJobGroupId() > 0) { + return properties.getXxlJob().getJobGroupId(); + } + String appname = defaultString(reportConfig.getXxlAppname(), properties.getXxlJob().getAppname()); + throw new IllegalStateException("XXL-JOB executor id is not configured, appname=" + appname); + } + + private Map jobInfoForm(int jobGroup, AgentReportJobConfigDTO jobConfig, Integer xxlJobId) { + Map form = new LinkedHashMap<>(); + if (xxlJobId != null) { + form.put("id", xxlJobId.toString()); + } + form.put("jobGroup", String.valueOf(jobGroup)); + form.put("jobDesc", defaultString(jobConfig.getName(), "Scheduled Job")); + form.put("author", "will-system"); + form.put("scheduleType", "CRON"); + form.put("scheduleConf", defaultString(jobConfig.getCron(), "0 0 10 * * ?")); + form.put("misfireStrategy", "DO_NOTHING"); + form.put("executorRouteStrategy", "FIRST"); + form.put("executorHandler", DISPATCH_HANDLER); + form.put("executorParam", "{\"configId\":\"" + jobConfig.getId() + "\"}"); + form.put("executorBlockStrategy", "SERIAL_EXECUTION"); + form.put("executorTimeout", "0"); + form.put("executorFailRetryCount", "0"); + form.put("glueType", "BEAN"); + form.put("glueSource", ""); + form.put("glueRemark", "GLUE init"); + form.put("childJobId", ""); + form.put("alarmEmail", jobConfig.getMailTo() == null ? "" : jobConfig.getMailTo()); + return form; + } + + private ClientSession login(AgentReportConfigDTO reportConfig) { + CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL); + HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .cookieHandler(cookieManager) + .build(); + List errors = new ArrayList<>(); + for (String adminBaseUrl : adminBaseUrlCandidates(reportConfig)) { + ClientSession session = new ClientSession(httpClient, adminBaseUrl); + try { + log.info("[XXL-JOB][Admin] try login baseUrl={}", adminBaseUrl); + login(session); + log.info("[XXL-JOB][Admin] login success, baseUrl={}", adminBaseUrl); + return session; + } catch (IllegalStateException ex) { + String message = adminBaseUrl + " -> " + ex.getMessage(); + errors.add(message); + log.warn("[XXL-JOB][Admin] login failed: {}", message); + } + } + throw new IllegalStateException("XXL-JOB Admin login failed, tried: " + String.join("; ", errors)); + } + + private void login(ClientSession session) { + postForm(session, "/auth/doLogin", Map.of( + "userName", defaultString(properties.getXxlJob().getAdminUsername(), "admin"), + "password", defaultString(properties.getXxlJob().getAdminPassword(), "123456"), + "ifRemember", "on" + )); + } + + private JsonNode postForm(ClientSession session, String path, Map form) { + return post(session, path, form); + } + + private JsonNode post(ClientSession session, String path, Map form) { + try { + String url = session.baseUrl() + path; + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(url)) + .timeout(Duration.ofSeconds(20)) + .header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + HttpRequest request = requestBuilder + .POST(HttpRequest.BodyPublishers.ofString(encodeForm(form), StandardCharsets.UTF_8)) + .build(); + HttpResponse response = session.httpClient().send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IllegalStateException("XXL-JOB HTTP status error: " + response.statusCode() + + ", path=" + path + ", body=" + abbreviate(response.body())); + } + JsonNode body = objectMapper.readTree(response.body()); + if (body.has("code") && body.path("code").asInt() != 200) { + String message = body.path("msg").asText(""); + if (message.contains("permission limit") && message.contains("role:ADMIN")) { + throw new IllegalStateException("XXL-JOB Admin account must have ADMIN role, path=" + path); + } + throw new IllegalStateException("XXL-JOB call failed: " + body); + } + return body; + } catch (Exception ex) { + throw new IllegalStateException("XXL-JOB Admin call failed: " + path + ", " + ex.getMessage(), ex); + } + } + + private String abbreviate(String value) { + if (value == null) { + return ""; + } + String normalized = value.replaceAll("\\s+", " ").trim(); + return normalized.length() <= 500 ? normalized : normalized.substring(0, 500); + } + + private String baseUrl(AgentReportConfigDTO reportConfig) { + String adminAddresses = defaultString(reportConfig.getXxlAdminAddresses(), properties.getXxlJob().getAdminAddresses()); + if (!StringUtils.hasText(adminAddresses)) { + throw new IllegalStateException("XXL-JOB admin addresses is not configured"); + } + String first = adminAddresses.split(",")[0].trim(); + return first.endsWith("/") ? first.substring(0, first.length() - 1) : first; + } + + private List adminBaseUrlCandidates(AgentReportConfigDTO reportConfig) { + return List.of(baseUrl(reportConfig)); + } + + private String encodeForm(Map form) { + StringBuilder builder = new StringBuilder(); + form.forEach((key, value) -> { + if (!builder.isEmpty()) { + builder.append('&'); + } + builder.append(URLEncoder.encode(key, StandardCharsets.UTF_8)); + builder.append('='); + builder.append(URLEncoder.encode(value == null ? "" : value, StandardCharsets.UTF_8)); + }); + return builder.toString(); + } + + private String defaultString(String value, String defaultValue) { + return StringUtils.hasText(value) ? value.trim() : defaultValue; + } + + private record ClientSession(HttpClient httpClient, String baseUrl) { + } +} diff --git a/coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobReportJobScheduleSyncService.java b/coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobReportJobScheduleSyncService.java new file mode 100644 index 0000000..f0aff64 --- /dev/null +++ b/coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobReportJobScheduleSyncService.java @@ -0,0 +1,35 @@ +package com.will.coreJob.framework.xxl; + +import com.will.businessJob.zentao.ZentaoWeComReportService; +import com.will.coreService.dto.AgentReportJobConfigDTO; +import com.will.coreService.service.AgentReportJobScheduleSyncService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class XxlJobReportJobScheduleSyncService implements AgentReportJobScheduleSyncService { + + private final XxlJobAdminClient xxlJobAdminClient; + private final ZentaoWeComReportService reportService; + + @Override + public AgentReportJobConfigDTO sync(AgentReportJobConfigDTO jobConfig) { + Integer xxlJobId = xxlJobAdminClient.saveJob(jobConfig); + jobConfig.setXxlJobId(xxlJobId); + return jobConfig; + } + + @Override + public void delete(AgentReportJobConfigDTO jobConfig) { + xxlJobAdminClient.removeJob(jobConfig); + } + + @Override + public String execute(AgentReportJobConfigDTO jobConfig) { + if (com.will.coreService.service.AgentReportJobConfigService.TYPE_ZENTAO_DAILY_EFFORT.equals(jobConfig.getJobType())) { + return reportService.sendDailyEffortReport(jobConfig); + } + throw new IllegalArgumentException("unsupported job type: " + jobConfig.getJobType()); + } +} diff --git a/coreJob/src/main/java/com/will/coreJob/framework/xxl/ZentaoEffortReportJobHandler.java b/coreJob/src/main/java/com/will/coreJob/framework/xxl/ZentaoEffortReportJobHandler.java new file mode 100644 index 0000000..9218314 --- /dev/null +++ b/coreJob/src/main/java/com/will/coreJob/framework/xxl/ZentaoEffortReportJobHandler.java @@ -0,0 +1,62 @@ +package com.will.coreJob.framework.xxl; + +import com.will.businessJob.zentao.ZentaoWeComReportService; +import com.will.coreService.dto.AgentReportJobConfigDTO; +import com.will.coreService.service.AgentReportJobConfigService; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ZentaoEffortReportJobHandler { + + private final AgentReportJobConfigService jobConfigService; + private final ZentaoWeComReportService reportService; + + @XxlJob("agentReportDispatchJob") + public void agentReportDispatchJob() { + try { + String configId = parseConfigId(XxlJobHelper.getJobParam()); + if (configId == null) { + throw new IllegalArgumentException("JobParam 必须包含 configId,例如 {\"configId\":\"xxx\"}"); + } + AgentReportJobConfigDTO config = jobConfigService.detail(configId); + String result = dispatch(config); + XxlJobHelper.handleSuccess(result); + } catch (Exception ex) { + log.warn("[XXL-JOB][AgentReport] dispatch failed", ex); + XxlJobHelper.handleFail(ex.getMessage()); + throw ex; + } + } + + private String dispatch(AgentReportJobConfigDTO config) { + if (AgentReportJobConfigService.TYPE_ZENTAO_DAILY_EFFORT.equals(config.getJobType())) { + return reportService.sendDailyEffortReport(config); + } + throw new IllegalArgumentException("不支持的任务类型:" + config.getJobType()); + } + + private String parseConfigId(String jobParam) { + if (jobParam == null || jobParam.isBlank()) { + return null; + } + String marker = "\"configId\""; + int markerIndex = jobParam.indexOf(marker); + if (markerIndex < 0) { + return jobParam.trim(); + } + int colonIndex = jobParam.indexOf(':', markerIndex); + int firstQuote = jobParam.indexOf('"', colonIndex + 1); + int secondQuote = jobParam.indexOf('"', firstQuote + 1); + if (colonIndex < 0 || firstQuote < 0 || secondQuote < 0) { + return null; + } + return jobParam.substring(firstQuote + 1, secondQuote).trim(); + } + +} diff --git a/coreQueue/build.gradle b/coreQueue/build.gradle new file mode 100644 index 0000000..92e43e9 --- /dev/null +++ b/coreQueue/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-amqp") +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/coreQueue/src/main/java/com/will/coreQueue/RabbitMqConnectionStartupValidator.java b/coreQueue/src/main/java/com/will/coreQueue/RabbitMqConnectionStartupValidator.java new file mode 100644 index 0000000..5f68c44 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/RabbitMqConnectionStartupValidator.java @@ -0,0 +1,41 @@ +package com.will.coreQueue; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.connection.Connection; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContextException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RabbitMqConnectionStartupValidator implements ApplicationRunner { + + private final ConnectionFactory connectionFactory; + + @Override + public void run(ApplicationArguments args) { + validateRabbitMqConnection(); + } + + private void validateRabbitMqConnection() { + log.info("🔍 验证RabbitMQ连接..."); + try { + Connection connection = connectionFactory.createConnection(); + boolean isConnected = connection != null && connection.isOpen(); + connection.close(); + + if (isConnected) { + log.info("✅ RabbitMQ连接正常"); + } else { + throw new RuntimeException("RabbitMQ连接无响应"); + } + } catch (Exception e) { + log.error("❌ RabbitMQ连接失败: {}", e.getMessage()); + throw new ApplicationContextException("RabbitMQ不可用,请检查配置", e); + } + } +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/config/ConditionalOnQueueEnabled.java b/coreQueue/src/main/java/com/will/coreQueue/config/ConditionalOnQueueEnabled.java new file mode 100644 index 0000000..bcfc650 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/config/ConditionalOnQueueEnabled.java @@ -0,0 +1,19 @@ +package com.will.coreQueue.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 仅在显式开启消息队列时加载 RabbitMQ 相关 Bean。 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ConditionalOnProperty(prefix = "will.queue", name = "enabled", havingValue = "true") +public @interface ConditionalOnQueueEnabled { +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumer.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumer.java new file mode 100644 index 0000000..67fbb3f --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumer.java @@ -0,0 +1,14 @@ +package com.will.coreQueue.core; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface QueueConsumer { + String queue(); + String value() default ""; +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerBeanRegistrar.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerBeanRegistrar.java new file mode 100644 index 0000000..89eb279 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerBeanRegistrar.java @@ -0,0 +1,71 @@ +package com.will.coreQueue.core; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.lang.NonNull; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class QueueConsumerBeanRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware { + + private Environment environment; + private ResourceLoader resourceLoader; + + @Override + public void registerBeanDefinitions(@NonNull AnnotationMetadata importingClassMetadata, @NonNull BeanDefinitionRegistry registry) { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false, this.environment); + provider.setResourceLoader(this.resourceLoader); + + AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(QueueConsumer.class); + provider.addIncludeFilter(annotationTypeFilter); + + String basePackage = Arrays.stream(getClass().getPackage().getName().split("\\.")).limit(2).collect(Collectors.joining(".")); + + Set beanDefinitions = provider.findCandidateComponents(basePackage); + for (BeanDefinition beanDefinition : beanDefinitions) { + if (beanDefinition instanceof AnnotatedBeanDefinition) { + AnnotationMetadata annotationMetadata = ((AnnotatedBeanDefinition) beanDefinition).getMetadata(); + if(annotationMetadata.isInterface()){ + System.out.println("QueueConsumerBeanRegistrar: " + annotationMetadata.getClassName()); + continue; + } + String className = annotationMetadata.getClassName(); + if (!registry.containsBeanDefinition(className)) { + RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(className); + rootBeanDefinition.setSynthetic(true); + + registry.registerBeanDefinition(className, rootBeanDefinition); + } + + String queueName = (String) Objects.requireNonNull(annotationMetadata.getAnnotationAttributes(QueueConsumer.class.getCanonicalName())).get("queue"); + QueueConsumerProvider.setQueueBean(queueName, className); + } + } + } + + @Override + public void setEnvironment(@NonNull Environment environment) { + this.environment = environment; + } + + @Override + public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerProvider.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerProvider.java new file mode 100644 index 0000000..f8592bc --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerProvider.java @@ -0,0 +1,35 @@ +package com.will.coreQueue.core; + + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class QueueConsumerProvider implements ApplicationContextAware { + ApplicationContext applicationContext; + + private static final ConcurrentHashMap consumerMap = new ConcurrentHashMap<>(); + + public QueueConsumerService getCustomerService(String queue) { + return (QueueConsumerService)applicationContext.getBean(consumerMap.get(queue)); + } + + public static void setQueueBean(String queue, String consumerBeanName) { + consumerMap.put(queue, consumerBeanName); + } + + public static Set getQueues() { + return consumerMap.keySet(); + } + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerService.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerService.java new file mode 100644 index 0000000..e0fc72f --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerService.java @@ -0,0 +1,7 @@ +package com.will.coreQueue.core; + +public abstract class QueueConsumerService { + + public abstract void process(QueueMessage message); + +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueListenerFactory.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueListenerFactory.java new file mode 100644 index 0000000..cdd10b0 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueListenerFactory.java @@ -0,0 +1,8 @@ +package com.will.coreQueue.core; + + +import java.util.Set; + +public interface QueueListenerFactory { + void addCommonConsumers(Set queues); +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueMessage.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueMessage.java new file mode 100644 index 0000000..8ec1f6c --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueMessage.java @@ -0,0 +1,13 @@ +package com.will.coreQueue.core; + + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class QueueMessage implements Serializable { + private String message; + private T body; + private String queue; +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueMessageListenerHandle.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueMessageListenerHandle.java new file mode 100644 index 0000000..3a16132 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueMessageListenerHandle.java @@ -0,0 +1,17 @@ +package com.will.coreQueue.core; + +import com.will.coreQueue.config.ConditionalOnQueueEnabled; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnQueueEnabled +public class QueueMessageListenerHandle { + @Autowired + QueueConsumerProvider queueConsumerProvider; + + public void handle(QueueMessage message) { + QueueConsumerService service = queueConsumerProvider.getCustomerService(message.getQueue()); + service.process(message); + } +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/core/QueueProducer.java b/coreQueue/src/main/java/com/will/coreQueue/core/QueueProducer.java new file mode 100644 index 0000000..abe8f6d --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/core/QueueProducer.java @@ -0,0 +1,7 @@ +package com.will.coreQueue.core; + +import org.springframework.stereotype.Component; + +public interface QueueProducer { + void send(QueueMessage message); +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/rabbit/MyRabbitConfigProperties.java b/coreQueue/src/main/java/com/will/coreQueue/rabbit/MyRabbitConfigProperties.java new file mode 100644 index 0000000..87662d5 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/rabbit/MyRabbitConfigProperties.java @@ -0,0 +1,35 @@ +package com.will.coreQueue.rabbit; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "spring.rabbitmq") +@Data +public class MyRabbitConfigProperties { + String host = "localhost"; + int port = 5672; + String username = "guest"; + String password = "guest"; + String virtualHost = "/"; + String connectionTimeout = "5000"; + int concurrencyConsumer = 10; + int maxConcurrencyConsumer = 10; + int prefetch = 1; + boolean gzipEnabled = false; + + boolean useSSL = false; + private Cache cache = new Cache(); + + @Data + public static class Cache { + private Channel channel = new Channel(); + + @Data + public static class Channel { + private int size = 10; // 默认 1 + } + } +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/rabbit/QueueModuleConfiguration.java b/coreQueue/src/main/java/com/will/coreQueue/rabbit/QueueModuleConfiguration.java new file mode 100644 index 0000000..1c3e589 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/rabbit/QueueModuleConfiguration.java @@ -0,0 +1,12 @@ +package com.will.coreQueue.rabbit; + +import com.will.coreQueue.config.ConditionalOnQueueEnabled; +import com.will.coreQueue.core.QueueConsumerBeanRegistrar; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@ConditionalOnQueueEnabled +@Configuration +@Import({QueueConsumerBeanRegistrar.class, RabbitAutoConfiguration.class}) +public class QueueModuleConfiguration { +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitAutoConfiguration.java b/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitAutoConfiguration.java new file mode 100644 index 0000000..743b63f --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitAutoConfiguration.java @@ -0,0 +1,72 @@ +package com.will.coreQueue.rabbit; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.postprocessor.GUnzipPostProcessor; +import org.springframework.amqp.support.postprocessor.GZipPostProcessor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(MyRabbitConfigProperties.class) +public class RabbitAutoConfiguration { + + private final MyRabbitConfigProperties myRabbitProperties; + + public RabbitAutoConfiguration(@Qualifier("myRabbitConfigProperties") MyRabbitConfigProperties myRabbitProperties) { + this.myRabbitProperties = myRabbitProperties; + } + + @Bean + @ConditionalOnMissingBean(ConnectionFactory.class) + public ConnectionFactory connectionFactory() { + CachingConnectionFactory factory = new CachingConnectionFactory(); + factory.setHost(myRabbitProperties.getHost()); + factory.setPort(myRabbitProperties.getPort()); + factory.setUsername(myRabbitProperties.getUsername()); + factory.setPassword(myRabbitProperties.getPassword()); + factory.setVirtualHost(myRabbitProperties.getVirtualHost()); + factory.setConnectionTimeout(Integer.parseInt(myRabbitProperties.getConnectionTimeout())); + + if (myRabbitProperties.getCache() != null + && myRabbitProperties.getCache().getChannel() != null) { + factory.setChannelCacheSize(myRabbitProperties.getCache().getChannel().getSize()); + } + + com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = factory.getRabbitConnectionFactory(); + rabbitConnectionFactory.setAutomaticRecoveryEnabled(true); + if (myRabbitProperties.isUseSSL()) { + try { + rabbitConnectionFactory.useSslProtocol(); + } catch (Exception e) { + throw new IllegalStateException("Failed to enable SSL for RabbitMQ connection", e); + } + } + return factory; + } + + @Bean + @ConditionalOnMissingBean(AmqpAdmin.class) + public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) { + return new RabbitAdmin(connectionFactory); + } + + @Bean + @ConditionalOnMissingBean(RabbitTemplate.class) + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(connectionFactory); + if (myRabbitProperties.isGzipEnabled()) { + template.setBeforePublishPostProcessors(new GZipPostProcessor()); + template.setAfterReceivePostProcessors(new GUnzipPostProcessor()); + } + template.setMessageConverter(new Jackson2JsonMessageConverter()); + return template; + } +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitListenerFactory.java b/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitListenerFactory.java new file mode 100644 index 0000000..1457594 --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitListenerFactory.java @@ -0,0 +1,88 @@ +package com.will.coreQueue.rabbit; + +import com.will.coreQueue.config.ConditionalOnQueueEnabled; +import com.will.coreQueue.core.QueueConsumerBeanRegistrar; +import com.will.coreQueue.core.QueueConsumerProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Slf4j +@Component +@ConditionalOnQueueEnabled +@RequiredArgsConstructor +public class RabbitListenerFactory implements com.will.coreQueue.core.QueueListenerFactory { + + private static final String DEFAULT_LISTENER_METHOD = "handle"; + private static final String DEFAULT_EXCHANGE = "WILL_EXCHANGE"; + + private final MyRabbitConfigProperties myRabbitProperties; + private final com.will.coreQueue.core.QueueMessageListenerHandle queueMessageHandle; + private final ConnectionFactory connectionFactory; + private final AmqpAdmin amqpAdmin; + + private SimpleMessageListenerContainer container; + + @EventListener(ApplicationReadyEvent.class) + public void declareQueues() { + DirectExchange exchange = new DirectExchange(DEFAULT_EXCHANGE, true, false); + amqpAdmin.declareExchange(exchange); + + Set queues = QueueConsumerProvider.getQueues(); + queues.forEach(queueName -> { + Queue queue = new Queue(queueName, true); + amqpAdmin.declareQueue(queue); + Binding binding = BindingBuilder.bind(queue).to(exchange).with(queueName); + amqpAdmin.declareBinding(binding); + }); + + if (!queues.isEmpty()) { + createListenerContainer(queues); + } else { + log.info("未注册 @QueueConsumer,跳过 RabbitMQ 监听器启动"); + } + } + + @Override + public void addCommonConsumers(Set queues) { + if (queues == null || queues.isEmpty()) { + return; + } + createListenerContainer(queues); + } + + private void createListenerContainer(Set queues) { + if (container != null && container.isRunning()) { + container.stop(); + } + + container = new SimpleMessageListenerContainer(connectionFactory); + container.setAutoDeclare(true); + container.setAmqpAdmin(amqpAdmin); + container.setConcurrentConsumers(myRabbitProperties.getConcurrencyConsumer()); + container.setMaxConcurrentConsumers(myRabbitProperties.getMaxConcurrencyConsumer()); + container.setPrefetchCount(myRabbitProperties.getPrefetch()); + container.setQueueNames(queues.toArray(new String[0])); + + MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(queueMessageHandle); + messageListenerAdapter.setDefaultListenerMethod(DEFAULT_LISTENER_METHOD); + messageListenerAdapter.setMessageConverter(new Jackson2JsonMessageConverter()); + container.setMessageListener(messageListenerAdapter); + container.start(); + + log.info("RabbitMQ 监听器已启动, queues={}", queues); + } +} diff --git a/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitProducer.java b/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitProducer.java new file mode 100644 index 0000000..1261e3f --- /dev/null +++ b/coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitProducer.java @@ -0,0 +1,23 @@ +package com.will.coreQueue.rabbit; + +import com.will.coreQueue.config.ConditionalOnQueueEnabled; +import com.will.coreQueue.core.QueueMessage; +import com.will.coreQueue.core.QueueProducer; +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnQueueEnabled +@RequiredArgsConstructor +public class RabbitProducer implements QueueProducer { + + private static final String DEFAULT_EXCHANGE = "WILL_EXCHANGE"; + + private final RabbitTemplate rabbitTemplate; + + @Override + public void send(QueueMessage message) { + rabbitTemplate.convertAndSend(DEFAULT_EXCHANGE, message.getQueue(), message); + } +} diff --git a/coreRedis/build.gradle b/coreRedis/build.gradle new file mode 100644 index 0000000..126c5f3 --- /dev/null +++ b/coreRedis/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java' +} + +dependencies { + + implementation("io.lettuce:lettuce-core") +} diff --git a/coreRedis/src/main/java/com/will/coreRedis/RedisConnectionStartupValidator.java b/coreRedis/src/main/java/com/will/coreRedis/RedisConnectionStartupValidator.java new file mode 100644 index 0000000..5a94b96 --- /dev/null +++ b/coreRedis/src/main/java/com/will/coreRedis/RedisConnectionStartupValidator.java @@ -0,0 +1,45 @@ +package com.will.coreRedis; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContextException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Component; + +import java.util.Objects; + + +@Component +@RequiredArgsConstructor +@Slf4j +public class RedisConnectionStartupValidator implements ApplicationRunner { + + private final RedisConnectionFactory redisConnectionFactory; + + @Override + public void run(ApplicationArguments args) throws Exception { + + validateRedisConnection(); + } + + private void validateRedisConnection() { + log.info("🔍 验证Redis连接..."); + try { + RedisConnection connection = redisConnectionFactory.getConnection(); + boolean isConnected = Objects.equals(connection.ping(), "PONG"); + connection.close(); + + if (isConnected) { + log.info("✅ Redis连接正常"); + } else { + throw new RuntimeException("Redis连接无响应"); + } + } catch (Exception e) { + log.error("❌ Redis连接失败: {}", e.getMessage()); + throw new ApplicationContextException("Redis不可用,请检查配置", e); + } + } +} diff --git a/coreRedis/src/main/java/com/will/coreRedis/config/RedisAdvancedConfig.java b/coreRedis/src/main/java/com/will/coreRedis/config/RedisAdvancedConfig.java new file mode 100644 index 0000000..5646847 --- /dev/null +++ b/coreRedis/src/main/java/com/will/coreRedis/config/RedisAdvancedConfig.java @@ -0,0 +1,95 @@ +package com.will.coreRedis.config; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.SocketOptions; +import io.lettuce.core.TimeoutOptions; +import io.lettuce.core.api.StatefulConnection; +import io.lettuce.core.resource.ClientResources; +import io.lettuce.core.resource.DefaultClientResources; +import lombok.RequiredArgsConstructor; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.*; +import java.time.Duration; + +@Configuration +@RequiredArgsConstructor +public class RedisAdvancedConfig { + private final RedisProperties redisProperties; + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + // 1. 创建 Redis 配置(单机模式) + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setHostName(redisProperties.getHost()); + config.setPort(redisProperties.getPort()); + config.setPassword(RedisPassword.of(redisProperties.getPassword())); + config.setDatabase(redisProperties.getDatabase()); + + // 2. 配置客户端选项 + ClientOptions clientOptions = ClientOptions.builder() + .autoReconnect(true) // 自动重连 + .pingBeforeActivateConnection(true) // 激活前 ping 检查 + .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) // 断连时拒绝命令 + .timeoutOptions(TimeoutOptions.enabled(redisProperties.getTimeout())) // 超时配置 + .socketOptions(SocketOptions.builder() + .connectTimeout(redisProperties.getConnectTimeout()) + .keepAlive(true) + .build()) + .build(); + + // 3. 配置连接池(使用正确的Lettuce连接池方式) + GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); + + poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive()); + poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle()); + poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle()); + poolConfig.setMaxWait(redisProperties.getLettuce().getPool().getMaxWait()); // 注意:新版使用setMaxWait方法 + poolConfig.setTestOnBorrow(true); // 获取连接时验证 + + // 4. 创建客户端资源(可选但推荐,用于管理线程池等资源) + ClientResources clientResources = DefaultClientResources.builder() + .ioThreadPoolSize(Runtime.getRuntime().availableProcessors()) + .build(); + + // 5. 创建客户端配置 + LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() + .clientOptions(clientOptions) + .clientResources(clientResources) // 设置客户端资源 + .commandTimeout(Duration.ofMillis(2000)) + .shutdownTimeout(Duration.ofMillis(100)) + .poolConfig(poolConfig) // 设置连接池配置 + .build(); + + return new LettuceConnectionFactory(config, clientConfig); + } + + // 6. 集群模式配置 + @Bean + @Profile("cluster") + public LettuceConnectionFactory redisClusterConnectionFactory() { + RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration( + redisProperties.getCluster().getNodes()); + + clusterConfig.setPassword(RedisPassword.of("clusterpassword")); + + // 集群模式也可以配置连接池 + GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); + + poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive()); + poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle()); + poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle()); + + LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() + .commandTimeout(Duration.ofSeconds(3)) + .poolConfig(poolConfig) + .build(); + + return new LettuceConnectionFactory(clusterConfig, clientConfig); + } +} \ No newline at end of file diff --git a/coreRedis/src/main/java/com/will/coreRedis/config/RedisTemplateConfig.java b/coreRedis/src/main/java/com/will/coreRedis/config/RedisTemplateConfig.java new file mode 100644 index 0000000..8480a77 --- /dev/null +++ b/coreRedis/src/main/java/com/will/coreRedis/config/RedisTemplateConfig.java @@ -0,0 +1,29 @@ +package com.will.coreRedis.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +@Configuration +@RequiredArgsConstructor +public class RedisTemplateConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + template.setKeySerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + template.afterPropertiesSet(); + return template; + } + +} \ No newline at end of file diff --git a/coreRedis/src/main/java/com/will/coreRedis/service/CacheService.java b/coreRedis/src/main/java/com/will/coreRedis/service/CacheService.java new file mode 100644 index 0000000..0cb7ded --- /dev/null +++ b/coreRedis/src/main/java/com/will/coreRedis/service/CacheService.java @@ -0,0 +1,110 @@ +package com.will.coreRedis.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CacheService { + + private final RedisTemplate redisTemplate; + + private static final String NULL_CACHE_PREFIX = "null:"; + private static final long NULL_CACHE_TTL = 60; // 空值缓存60秒 + + /** + * 带防护的缓存获取 + */ + public T getWithProtection(String key, Class type, Supplier dbLoader) { + try { + // 1. 检查空缓存 + if (isNullOrEmptyCache(key)) { + return null; + } + + // 2. 尝试从缓存获取 + ValueOperations ops = redisTemplate.opsForValue(); + Object cached = ops.get(key); + + if (cached != null) { + return type.cast(cached); + } + + // 3. 布隆过滤器检查(防止缓存穿透) + if (!bloomFilterMightContain(key)) { + setNullCache(key); + return null; + } + + // 4. 分布式锁防止缓存雪崩 + String lockKey = "lock:cache:" + key; + String requestId = UUID.randomUUID().toString(); + + try { + if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS))) { + // 再次检查缓存(双重检查) + cached = ops.get(key); + if (cached != null) { + return type.cast(cached); + } + + // 5. 从数据库加载 + T value = dbLoader.get(); + + if (value == null) { + // 设置空缓存防止穿透 + setNullCache(key); + return null; + } + + // 6. 设置缓存(随机过期时间防止雪崩) + long ttl = calculateRandomTtl(300, 60); // 5分钟 ± 1分钟 + ops.set(key, value, ttl, TimeUnit.SECONDS); + + return value; + } + } finally { + // 7. 释放锁 + redisTemplate.delete(lockKey); + } + + // 8. 降级策略:如果获取锁失败,短暂等待后重试或返回旧数据 + Thread.sleep(50); + cached = ops.get(key); + return cached != null ? type.cast(cached) : dbLoader.get(); + + } catch (Exception e) { + // 9. 熔断降级 + log.error("缓存读取失败,降级处理: {}", key, e); + return dbLoader.get(); + } + } + + private boolean isNullOrEmptyCache(String key) { + Boolean isNullCache = redisTemplate.hasKey(NULL_CACHE_PREFIX + key); + return Boolean.TRUE.equals(isNullCache); + } + + private void setNullCache(String key) { + redisTemplate.opsForValue().set(NULL_CACHE_PREFIX + key, "1", NULL_CACHE_TTL, TimeUnit.SECONDS); + } + + private long calculateRandomTtl(long baseSeconds, long randomRange) { + return baseSeconds + ThreadLocalRandom.current().nextLong(-randomRange, randomRange); + } + + // 布隆过滤器实现(简化版) + private boolean bloomFilterMightContain(String key) { + // 实际项目应使用 RedisBloom 模块 + return true; // 默认允许查询 + } +} \ No newline at end of file diff --git a/coreRedis/src/main/java/com/will/coreRedis/service/RedisDistributedLock.java b/coreRedis/src/main/java/com/will/coreRedis/service/RedisDistributedLock.java new file mode 100644 index 0000000..d12f11f --- /dev/null +++ b/coreRedis/src/main/java/com/will/coreRedis/service/RedisDistributedLock.java @@ -0,0 +1,96 @@ +package com.will.coreRedis.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisDistributedLock { + + private final StringRedisTemplate redisTemplate; + private static final String LOCK_KEY_PREFIX = "lock:"; + + /** + * 尝试获取分布式锁 + * @param lockName 锁名称 + * @param requestId 请求ID(唯一标识) + * @param expireTime 过期时间(毫秒) + * @return 是否获取成功 + */ + public boolean tryLock(String lockName, String requestId, long expireTime) { + String key = LOCK_KEY_PREFIX + lockName; + Boolean result = redisTemplate.opsForValue().setIfAbsent(key, requestId, expireTime, TimeUnit.MILLISECONDS); + return Boolean.TRUE.equals(result); + } + + /** + * 带重试的锁获取 + */ + public boolean tryLockWithRetry(String lockName, String requestId, long expireTime, + int retryTimes, long retryDelay) throws InterruptedException { + for (int i = 0; i < retryTimes; i++) { + if (tryLock(lockName, requestId, expireTime)) { + return true; + } + Thread.sleep(retryDelay); + } + return false; + } + + /** + * 安全释放锁(Lua 脚本保证原子性) + */ + public boolean releaseLock(String lockName, String requestId) { + String key = LOCK_KEY_PREFIX + lockName; + + // Lua 脚本确保原子操作 + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + + " return redis.call('del', KEYS[1]) " + + "else " + + " return 0 " + + "end"; + + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(script); + redisScript.setResultType(Long.class); + + Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), requestId); + return result != null && result == 1L; + } + + /** + * 自动续期锁(看门狗模式) + */ + @Scheduled(fixedRate = 10000) // 每10秒检查一次 + public void autoRenewLocks() { + Set keys = redisTemplate.keys(LOCK_KEY_PREFIX + "*"); + if (keys == null || keys.isEmpty()) { + return; + } + + for (String key : keys) { + try { + Long ttl = redisTemplate.getExpire(key); + // 如果 TTL 小于 30 秒,续期到 60 秒 + if (ttl != null && ttl > 0 && ttl < 30) { + String requestId = redisTemplate.opsForValue().get(key); + if (requestId != null) { + redisTemplate.expire(key, 60, TimeUnit.SECONDS); + } + } + } catch (Exception e) { + // 记录异常但不中断其他锁 + log.warn("自动续期锁失败: {}", key, e); + } + } + } +} \ No newline at end of file diff --git a/coreRedis/src/main/java/com/will/coreRedis/service/StringRedisService.java b/coreRedis/src/main/java/com/will/coreRedis/service/StringRedisService.java new file mode 100644 index 0000000..6c975b1 --- /dev/null +++ b/coreRedis/src/main/java/com/will/coreRedis/service/StringRedisService.java @@ -0,0 +1,40 @@ +package com.will.coreRedis.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class StringRedisService { + + private final StringRedisTemplate stringRedisTemplate; + + // 简单的键值操作 + public void set(String key, String value) { + stringRedisTemplate.opsForValue().set(key, value); + } + + public void setWithExpiry(String key, String value, long timeout, TimeUnit unit) { + stringRedisTemplate.opsForValue().set(key, value, timeout, unit); + } + + public String get(String key) { + return stringRedisTemplate.opsForValue().get(key); + } + + public Boolean delete(String key) { + return stringRedisTemplate.delete(key); + } + + // 原子操作 + public Long increment(String key) { + return stringRedisTemplate.opsForValue().increment(key); + } + + public Long decrement(String key) { + return stringRedisTemplate.opsForValue().decrement(key); + } +} diff --git a/coreService/build.gradle b/coreService/build.gradle new file mode 100644 index 0000000..d9290ec --- /dev/null +++ b/coreService/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(":coreData") + implementation project(":coreCommon") + implementation project(":coreQueue") + implementation project(":coreRedis") + + implementation("com.baomidou:mybatis-plus-jsqlparser:3.5.14") + implementation("com.baomidou:mybatis-plus-spring-boot3-starter:3.5.14") +} diff --git a/coreService/src/main/java/com/will/coreService/IBaseService.java b/coreService/src/main/java/com/will/coreService/IBaseService.java new file mode 100644 index 0000000..db20585 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/IBaseService.java @@ -0,0 +1,18 @@ +package com.will.coreService; + +import com.will.coreCommon.BasePage; + +import java.util.List; + +public interface IBaseService { + + BasePage getPageDto(T condition, Integer pageIndex, Integer pageSize); + + boolean save(T entity); + + boolean updateById(T entity); + + boolean removeById(String id); + + boolean removeBatchByIds(List ids); +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentChatMessageDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentChatMessageDTO.java new file mode 100644 index 0000000..b3aafe6 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentChatMessageDTO.java @@ -0,0 +1,15 @@ +package com.will.coreService.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class AgentChatMessageDTO { + + private String id; + private String role; + private String content; + private Integer sortOrder; + private LocalDateTime created; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentChatMessageSaveDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentChatMessageSaveDTO.java new file mode 100644 index 0000000..3765fc2 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentChatMessageSaveDTO.java @@ -0,0 +1,11 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class AgentChatMessageSaveDTO { + + private String role; + private String content; + private Integer sortOrder; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentChatSessionDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentChatSessionDTO.java new file mode 100644 index 0000000..0ddc454 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentChatSessionDTO.java @@ -0,0 +1,18 @@ +package com.will.coreService.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class AgentChatSessionDTO { + + private String id; + private String title; + private String scope; + private String backendSessionId; + private LocalDateTime created; + private LocalDateTime lastUpdated; + private List messages; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentChatSessionSaveDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentChatSessionSaveDTO.java new file mode 100644 index 0000000..a769682 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentChatSessionSaveDTO.java @@ -0,0 +1,12 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class AgentChatSessionSaveDTO { + + private String id; + private String title; + private String scope; + private String backendSessionId; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentReportConfigDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentReportConfigDTO.java new file mode 100644 index 0000000..9a84b96 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentReportConfigDTO.java @@ -0,0 +1,24 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class AgentReportConfigDTO { + + private Boolean enabled; + private String robotType; + private String webhookUrl; + private String mailTo; + private String mailSubject; + private Boolean dailyEffortEnabled; + private Boolean localSchedulerEnabled; + private String cron; + private String zone; + private Integer daysAgo; + private String groupName; + private Boolean xxlJobEnabled; + private String xxlAdminAddresses; + private Integer xxlJobGroupId; + private String xxlAppname; + private Integer xxlExecutorPort; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentReportJobConfigDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentReportJobConfigDTO.java new file mode 100644 index 0000000..a82cf3c --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentReportJobConfigDTO.java @@ -0,0 +1,21 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class AgentReportJobConfigDTO { + + private String id; + private String name; + private Boolean enabled; + private String jobType; + private String cron; + private String zone; + private String robotType; + private String webhookUrl; + private String mailTo; + private String mailSubject; + private Integer xxlJobId; + private Boolean localSchedulerEnabled; + private String remark; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentReportJobTypeDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentReportJobTypeDTO.java new file mode 100644 index 0000000..0a3be78 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentReportJobTypeDTO.java @@ -0,0 +1,13 @@ +package com.will.coreService.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AgentReportJobTypeDTO { + + private String value; + private String label; + private String description; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/AgentTaskFunctionDTO.java b/coreService/src/main/java/com/will/coreService/dto/AgentTaskFunctionDTO.java new file mode 100644 index 0000000..8a6eaac --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/AgentTaskFunctionDTO.java @@ -0,0 +1,14 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class AgentTaskFunctionDTO { + + private String id; + private String code; + private String name; + private String description; + private Boolean enabled; + private Integer sortOrder; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/BaseService.java b/coreService/src/main/java/com/will/coreService/dto/BaseService.java new file mode 100644 index 0000000..9534c2b --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/BaseService.java @@ -0,0 +1,39 @@ +package com.will.coreService.dto; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.will.coreCommon.BasePage; +import com.will.coreService.IBaseService; + +import java.util.List; + +public class BaseService implements IBaseService { + + @Override + public BasePage getPageDto(T condition, Integer pageIndex, Integer pageSize) { + + + + Page page = new Page<>(pageIndex, pageSize); + return null; + } + + @Override + public boolean save(T entity) { + return false; + } + + @Override + public boolean updateById(T entity) { + return false; + } + + @Override + public boolean removeById(String id) { + return false; + } + + @Override + public boolean removeBatchByIds(List ids) { + return false; + } +} diff --git a/coreService/src/main/java/com/will/coreService/dto/DataPermissionDTO.java b/coreService/src/main/java/com/will/coreService/dto/DataPermissionDTO.java new file mode 100644 index 0000000..4f628b8 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/DataPermissionDTO.java @@ -0,0 +1,14 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class DataPermissionDTO { + private String id; + private String permissionId; + private String roleId; + private String userId; + private String readPermission; + private String editPermission; + private String deletePermission; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/DataPermissionListItemDTO.java b/coreService/src/main/java/com/will/coreService/dto/DataPermissionListItemDTO.java new file mode 100644 index 0000000..2de25ad --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/DataPermissionListItemDTO.java @@ -0,0 +1,14 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class DataPermissionListItemDTO { + private String id; + private String roleId; + private String roleCode; + private String roleName; + private String readPermission; + private String editPermission; + private String deletePermission; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/DataPermissionSaveDTO.java b/coreService/src/main/java/com/will/coreService/dto/DataPermissionSaveDTO.java new file mode 100644 index 0000000..bbe7938 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/DataPermissionSaveDTO.java @@ -0,0 +1,12 @@ +package com.will.coreService.dto; + +import lombok.Data; + +@Data +public class DataPermissionSaveDTO { + private String roleId; + /** ALL | DEPT | DEPT_AND_CHILD | SELF */ + private String readPermission; + private String editPermission; + private String deletePermission; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/DepartmentDTO.java b/coreService/src/main/java/com/will/coreService/dto/DepartmentDTO.java new file mode 100644 index 0000000..80bd503 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/DepartmentDTO.java @@ -0,0 +1,20 @@ +package com.will.coreService.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class DepartmentDTO { + String departmentName; + String departmentCode; + String parentId; + String id; + String remark; + String created; + String createdBy; + Integer status; + String lastUpdated; + String lastUpdatedBy; + List children; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/LLMModelDTO.java b/coreService/src/main/java/com/will/coreService/dto/LLMModelDTO.java new file mode 100644 index 0000000..7642413 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/LLMModelDTO.java @@ -0,0 +1,24 @@ +package com.will.coreService.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class LLMModelDTO { + private String id; + private String name; + private String provider; + private String modelKey; + private String apiKey; + private String apiUrl; + private String maxTokens; + private String temperature; + private String status; + private String description; + + private LocalDateTime created; + private String createdBy; + private LocalDateTime lastUpdated; + private String lastUpdatedBy; +} \ No newline at end of file diff --git a/coreService/src/main/java/com/will/coreService/dto/MenuDTO.java b/coreService/src/main/java/com/will/coreService/dto/MenuDTO.java new file mode 100644 index 0000000..59f9196 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/MenuDTO.java @@ -0,0 +1,44 @@ +package com.will.coreService.dto; + + + +import lombok.Data; + + + +import java.util.List; + + + +@Data + +public class MenuDTO { + + private String id; + + private String code; + + private String name; + + private String parentId; + + private String menuType; + + private String routePath; + + private String routeName; + + private String component; + + private String redirect; + + private String icon; + + private Integer sortOrder; + + private String title; + + private List children; + +} + diff --git a/coreService/src/main/java/com/will/coreService/dto/RoleDTO.java b/coreService/src/main/java/com/will/coreService/dto/RoleDTO.java new file mode 100644 index 0000000..ab66dd3 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/RoleDTO.java @@ -0,0 +1,14 @@ +package com.will.coreService.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class RoleDTO { + private String id; + private String code; + private String name; + private String departmentId; + private List menuIds; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/UserDTO.java b/coreService/src/main/java/com/will/coreService/dto/UserDTO.java new file mode 100644 index 0000000..a3547ca --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/UserDTO.java @@ -0,0 +1,20 @@ +package com.will.coreService.dto; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class UserDTO { + String id; + String account; + String userName; + String email; + Integer phoneNumber; + Boolean status; + LocalDateTime lastLoginTime; + + LocalDateTime created; + String createdBy; + LocalDateTime lastUpdated; + String lastUpdatedBy; +} diff --git a/coreService/src/main/java/com/will/coreService/dto/UserSaveDTO.java b/coreService/src/main/java/com/will/coreService/dto/UserSaveDTO.java new file mode 100644 index 0000000..7526ac1 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/dto/UserSaveDTO.java @@ -0,0 +1,19 @@ +package com.will.coreService.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class UserSaveDTO { + private String id; + private String account; + private String userName; + private String email; + private Integer phoneNumber; + private Boolean status; + /** 新建或重置密码时传入 */ + private String password; + private List departmentIds; + private List roleIds; +} diff --git a/coreService/src/main/java/com/will/coreService/enums/RobotType.java b/coreService/src/main/java/com/will/coreService/enums/RobotType.java new file mode 100644 index 0000000..1775358 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/enums/RobotType.java @@ -0,0 +1,32 @@ +package com.will.coreService.enums; + +import org.springframework.util.StringUtils; + +public enum RobotType { + WE_COM("企业微信"), + FEISHU("飞书"), + DINGTALK("钉钉"), + EMAIL("Email"); + + private final String label; + + RobotType(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + + public static String defaultName(String robotType) { + if (!StringUtils.hasText(robotType)) { + return WE_COM.name(); + } + for (RobotType value : values()) { + if (value.name().equalsIgnoreCase(robotType.trim())) { + return value.name(); + } + } + return WE_COM.name(); + } +} diff --git a/coreService/src/main/java/com/will/coreService/service/AgentChatHistoryService.java b/coreService/src/main/java/com/will/coreService/service/AgentChatHistoryService.java new file mode 100644 index 0000000..e4ef504 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/AgentChatHistoryService.java @@ -0,0 +1,173 @@ +package com.will.coreService.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.will.coreCommon.context.UserContext; +import com.will.coreData.dao.IAgentChatMessageService; +import com.will.coreData.dao.IAgentChatSessionService; +import com.will.coreData.entity.AgentChatMessage; +import com.will.coreData.entity.AgentChatSession; +import com.will.coreService.dto.AgentChatMessageDTO; +import com.will.coreService.dto.AgentChatMessageSaveDTO; +import com.will.coreService.dto.AgentChatSessionDTO; +import com.will.coreService.dto.AgentChatSessionSaveDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class AgentChatHistoryService { + + private static final int DEFAULT_LIMIT = 50; + + private final IAgentChatSessionService sessionService; + private final IAgentChatMessageService messageService; + + public List listSessions(Integer limit) { + String userId = currentUserId(); + int pageSize = limit == null || limit <= 0 ? DEFAULT_LIMIT : Math.min(limit, DEFAULT_LIMIT); + return sessionService.list(new LambdaQueryWrapper() + .eq(AgentChatSession::getUserId, userId) + .eq(AgentChatSession::getDeleted, Boolean.FALSE) + .orderByDesc(AgentChatSession::getLastUpdated) + .last("LIMIT " + pageSize)) + .stream() + .map(session -> toSessionDTO(session, false)) + .toList(); + } + + public AgentChatSessionDTO detail(String id) { + AgentChatSession session = requireOwnedSession(id); + return toSessionDTO(session, true); + } + + @Transactional + public AgentChatSessionDTO saveSession(AgentChatSessionSaveDTO dto) { + String userId = currentUserId(); + AgentChatSession session; + if (dto != null && StringUtils.hasText(dto.getId())) { + session = requireOwnedSession(dto.getId()); + } else { + session = new AgentChatSession(); + session.setId(UUID.randomUUID().toString().replace("-", "")); + session.setUserId(userId); + session.setDeleted(Boolean.FALSE); + } + session.setTitle(resolveTitle(dto == null ? null : dto.getTitle())); + session.setScope(resolveScope(dto == null ? null : dto.getScope())); + if (dto != null && StringUtils.hasText(dto.getBackendSessionId())) { + session.setBackendSessionId(dto.getBackendSessionId()); + } + sessionService.saveOrUpdate(session); + return toSessionDTO(session, true); + } + + @Transactional + public AgentChatMessageDTO appendMessage(String sessionId, AgentChatMessageSaveDTO dto) { + AgentChatSession session = requireOwnedSession(sessionId); + AgentChatMessage message = new AgentChatMessage(); + message.setId(UUID.randomUUID().toString().replace("-", "")); + message.setSessionId(session.getId()); + message.setUserId(currentUserId()); + message.setRole(resolveRole(dto == null ? null : dto.getRole())); + message.setContent(dto == null ? "" : dto.getContent()); + message.setSortOrder(dto != null && dto.getSortOrder() != null ? dto.getSortOrder() : nextSortOrder(session.getId())); + messageService.save(message); + + if ("user".equals(message.getRole())) { + session.setTitle(resolveTitle(message.getContent())); + } + sessionService.updateById(session); + return toMessageDTO(message); + } + + @Transactional + public void clearMessages(String sessionId) { + AgentChatSession session = requireOwnedSession(sessionId); + messageService.remove(new LambdaQueryWrapper() + .eq(AgentChatMessage::getSessionId, session.getId()) + .eq(AgentChatMessage::getUserId, currentUserId())); + session.setTitle("新会话"); + sessionService.updateById(session); + } + + @Transactional + public void deleteSession(String sessionId) { + AgentChatSession session = requireOwnedSession(sessionId); + session.setDeleted(Boolean.TRUE); + sessionService.updateById(session); + } + + private AgentChatSession requireOwnedSession(String id) { + if (!StringUtils.hasText(id)) { + throw new IllegalArgumentException("sessionId 不能为空"); + } + AgentChatSession session = sessionService.getOne(new LambdaQueryWrapper() + .eq(AgentChatSession::getId, id) + .eq(AgentChatSession::getUserId, currentUserId()) + .eq(AgentChatSession::getDeleted, Boolean.FALSE), false); + if (session == null) { + throw new IllegalArgumentException("会话不存在或无权访问"); + } + return session; + } + + private int nextSortOrder(String sessionId) { + AgentChatMessage latest = messageService.getOne(new LambdaQueryWrapper() + .eq(AgentChatMessage::getSessionId, sessionId) + .orderByDesc(AgentChatMessage::getSortOrder) + .last("LIMIT 1"), false); + return latest == null || latest.getSortOrder() == null ? 1 : latest.getSortOrder() + 1; + } + + private AgentChatSessionDTO toSessionDTO(AgentChatSession session, boolean includeMessages) { + AgentChatSessionDTO dto = new AgentChatSessionDTO(); + BeanUtils.copyProperties(session, dto); + if (includeMessages) { + dto.setMessages(messageService.list(new LambdaQueryWrapper() + .eq(AgentChatMessage::getSessionId, session.getId()) + .eq(AgentChatMessage::getUserId, session.getUserId()) + .orderByAsc(AgentChatMessage::getSortOrder) + .orderByAsc(AgentChatMessage::getCreated)) + .stream() + .map(this::toMessageDTO) + .toList()); + } + return dto; + } + + private AgentChatMessageDTO toMessageDTO(AgentChatMessage message) { + AgentChatMessageDTO dto = new AgentChatMessageDTO(); + BeanUtils.copyProperties(message, dto); + return dto; + } + + private String currentUserId() { + String userId = UserContext.getCurrentUserId(); + if (!StringUtils.hasText(userId)) { + return "anonymous"; + } + return userId; + } + + private String resolveTitle(String title) { + if (!StringUtils.hasText(title)) { + return "新会话"; + } + String trimmed = title.trim(); + return trimmed.length() > 60 ? trimmed.substring(0, 60) : trimmed; + } + + private String resolveScope(String scope) { + return "system".equals(scope) ? "system" : "zentao"; + } + + private String resolveRole(String role) { + return "assistant".equals(role) ? "assistant" : "user"; + } +} diff --git a/coreService/src/main/java/com/will/coreService/service/AgentReportConfigService.java b/coreService/src/main/java/com/will/coreService/service/AgentReportConfigService.java new file mode 100644 index 0000000..b62130e --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/AgentReportConfigService.java @@ -0,0 +1,88 @@ +package com.will.coreService.service; + +import com.will.coreData.dao.IAgentReportConfigService; +import com.will.coreData.entity.AgentReportConfig; +import com.will.coreService.dto.AgentReportConfigDTO; +import com.will.coreService.enums.RobotType; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +@Service +@RequiredArgsConstructor +public class AgentReportConfigService { + + public static final String DEFAULT_ID = "zentao-daily-effort-wecom"; + + private final IAgentReportConfigService configService; + + public AgentReportConfigDTO getConfig() { + return toDTO(getOrCreate()); + } + + @Transactional + public AgentReportConfigDTO saveConfig(AgentReportConfigDTO dto) { + AgentReportConfig config = getOrCreate(); + config.setEnabled(defaultBoolean(dto.getEnabled(), true)); + config.setRobotType(RobotType.defaultName(dto.getRobotType())); + config.setWebhookUrl(trimToNull(dto.getWebhookUrl())); + config.setMailTo(trimToNull(dto.getMailTo())); + config.setMailSubject(trimToNull(dto.getMailSubject())); + config.setDailyEffortEnabled(defaultBoolean(dto.getDailyEffortEnabled(), true)); + config.setLocalSchedulerEnabled(defaultBoolean(dto.getLocalSchedulerEnabled(), true)); + config.setCron(defaultString(dto.getCron(), "0 0 10 * * ?")); + config.setZone(defaultString(dto.getZone(), "Asia/Shanghai")); + config.setDaysAgo(dto.getDaysAgo() == null || dto.getDaysAgo() < 0 ? 2 : dto.getDaysAgo()); + config.setGroupName(defaultString(dto.getGroupName(), "TSO")); + config.setXxlJobEnabled(defaultBoolean(dto.getXxlJobEnabled(), false)); + config.setXxlAdminAddresses(trimToNull(dto.getXxlAdminAddresses())); + config.setXxlJobGroupId(dto.getXxlJobGroupId()); + config.setXxlAppname(defaultString(dto.getXxlAppname(), "will-system-agent")); + config.setXxlExecutorPort(dto.getXxlExecutorPort() == null ? 9999 : dto.getXxlExecutorPort()); + configService.saveOrUpdate(config); + return toDTO(config); + } + + private AgentReportConfig getOrCreate() { + AgentReportConfig config = configService.getById(DEFAULT_ID); + if (config != null) { + return config; + } + config = new AgentReportConfig(); + config.setId(DEFAULT_ID); + config.setEnabled(true); + config.setRobotType(RobotType.WE_COM.name()); + config.setDailyEffortEnabled(true); + config.setLocalSchedulerEnabled(true); + config.setCron("0 0 10 * * ?"); + config.setZone("Asia/Shanghai"); + config.setDaysAgo(2); + config.setGroupName("TSO"); + config.setXxlJobEnabled(false); + config.setXxlAppname("will-system-agent"); + config.setXxlExecutorPort(9999); + configService.save(config); + return config; + } + + private AgentReportConfigDTO toDTO(AgentReportConfig config) { + AgentReportConfigDTO dto = new AgentReportConfigDTO(); + BeanUtils.copyProperties(config, dto); + dto.setRobotType(RobotType.defaultName(dto.getRobotType())); + return dto; + } + + private Boolean defaultBoolean(Boolean value, boolean defaultValue) { + return value == null ? defaultValue : value; + } + + private String defaultString(String value, String defaultValue) { + return StringUtils.hasText(value) ? value.trim() : defaultValue; + } + + private String trimToNull(String value) { + return StringUtils.hasText(value) ? value.trim() : null; + } +} diff --git a/coreService/src/main/java/com/will/coreService/service/AgentReportJobConfigService.java b/coreService/src/main/java/com/will/coreService/service/AgentReportJobConfigService.java new file mode 100644 index 0000000..e7527c2 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/AgentReportJobConfigService.java @@ -0,0 +1,150 @@ +package com.will.coreService.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.will.coreData.dao.IAgentReportJobConfigService; +import com.will.coreData.entity.AgentReportJobConfig; +import com.will.coreService.dto.AgentReportJobConfigDTO; +import com.will.coreService.dto.AgentReportJobTypeDTO; +import com.will.coreService.enums.RobotType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.UUID; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AgentReportJobConfigService { + + public static final String TYPE_ZENTAO_DAILY_EFFORT = "ZENTAO_DAILY_EFFORT"; + + private final IAgentReportJobConfigService jobConfigService; + private final AgentTaskFunctionService taskFunctionService; + private final ObjectProvider scheduleSyncService; + + public List list() { + return list(null, null, null); + } + + public List list(String name, String jobType, Boolean enabled) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .like(StringUtils.hasText(name), AgentReportJobConfig::getName, name == null ? null : name.trim()) + .eq(StringUtils.hasText(jobType), AgentReportJobConfig::getJobType, jobType == null ? null : jobType.trim()) + .eq(enabled != null, AgentReportJobConfig::getEnabled, enabled) + .orderByDesc(AgentReportJobConfig::getLastUpdated); + return jobConfigService.list(queryWrapper) + .stream() + .map(this::toDTO) + .toList(); + } + + public List jobTypes() { + return taskFunctionService.enabledJobTypes(); + } + + public List listLocalEnabled() { + return jobConfigService.list(new LambdaQueryWrapper() + .eq(AgentReportJobConfig::getEnabled, Boolean.TRUE) + .eq(AgentReportJobConfig::getLocalSchedulerEnabled, Boolean.TRUE)) + .stream() + .map(this::toDTO) + .toList(); + } + + public AgentReportJobConfigDTO detail(String id) { + AgentReportJobConfig config = jobConfigService.getById(id); + if (config == null) { + throw new IllegalArgumentException("任务配置不存在"); + } + return toDTO(config); + } + + @Transactional + public AgentReportJobConfigDTO save(AgentReportJobConfigDTO dto) { + AgentReportJobConfig config = StringUtils.hasText(dto.getId()) + ? jobConfigService.getById(dto.getId()) + : null; + if (config == null) { + config = new AgentReportJobConfig(); + config.setId(UUID.randomUUID().toString().replace("-", "")); + } + config.setName(defaultString(dto.getName(), "禅道工时报表")); + config.setEnabled(defaultBoolean(dto.getEnabled(), true)); + config.setJobType(defaultString(dto.getJobType(), TYPE_ZENTAO_DAILY_EFFORT)); + config.setCron(defaultString(dto.getCron(), "0 0 10 * * ?")); + config.setZone(defaultString(dto.getZone(), "Asia/Shanghai")); + config.setRobotType(trimRobotTypeToNull(dto.getRobotType())); + config.setWebhookUrl(trimToNull(dto.getWebhookUrl())); + config.setMailTo(trimToNull(dto.getMailTo())); + config.setMailSubject(trimToNull(dto.getMailSubject())); + config.setLocalSchedulerEnabled(false); + config.setRemark(trimToNull(dto.getRemark())); + jobConfigService.saveOrUpdate(config); + AgentReportJobConfigDTO saved = toDTO(config); + AgentReportJobConfigDTO synced = syncSchedule(saved); + if (synced.getXxlJobId() != null && !synced.getXxlJobId().equals(config.getXxlJobId())) { + config.setXxlJobId(synced.getXxlJobId()); + jobConfigService.updateById(config); + saved = toDTO(config); + } + return saved; + } + + @Transactional + public void delete(String id) { + AgentReportJobConfigDTO existing = detail(id); + scheduleSyncService.ifAvailable(service -> service.delete(existing)); + jobConfigService.removeById(id); + } + + public String execute(String id) { + AgentReportJobConfigDTO existing = detail(id); + AgentReportJobScheduleSyncService service = scheduleSyncService.getIfAvailable(); + if (service == null) { + throw new IllegalStateException("计划任务执行服务未启用"); + } + return service.execute(existing); + } + + private AgentReportJobConfigDTO syncSchedule(AgentReportJobConfigDTO jobConfig) { + AgentReportJobScheduleSyncService service = scheduleSyncService.getIfAvailable(); + if (service == null) { + return jobConfig; + } + try { + return service.sync(jobConfig); + } catch (Exception ex) { + log.warn("[AgentReportJob] sync XXL-JOB failed, jobId={}, name={}", + jobConfig.getId(), jobConfig.getName(), ex); + throw ex; + } + } + + private AgentReportJobConfigDTO toDTO(AgentReportJobConfig config) { + AgentReportJobConfigDTO dto = new AgentReportJobConfigDTO(); + BeanUtils.copyProperties(config, dto); + return dto; + } + + private Boolean defaultBoolean(Boolean value, boolean defaultValue) { + return value == null ? defaultValue : value; + } + + private String defaultString(String value, String defaultValue) { + return StringUtils.hasText(value) ? value.trim() : defaultValue; + } + + private String trimToNull(String value) { + return StringUtils.hasText(value) ? value.trim() : null; + } + + private String trimRobotTypeToNull(String value) { + return StringUtils.hasText(value) ? RobotType.defaultName(value) : null; + } +} diff --git a/coreService/src/main/java/com/will/coreService/service/AgentReportJobScheduleSyncService.java b/coreService/src/main/java/com/will/coreService/service/AgentReportJobScheduleSyncService.java new file mode 100644 index 0000000..22a9057 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/AgentReportJobScheduleSyncService.java @@ -0,0 +1,12 @@ +package com.will.coreService.service; + +import com.will.coreService.dto.AgentReportJobConfigDTO; + +public interface AgentReportJobScheduleSyncService { + + AgentReportJobConfigDTO sync(AgentReportJobConfigDTO jobConfig); + + void delete(AgentReportJobConfigDTO jobConfig); + + String execute(AgentReportJobConfigDTO jobConfig); +} diff --git a/coreService/src/main/java/com/will/coreService/service/AgentTaskFunctionService.java b/coreService/src/main/java/com/will/coreService/service/AgentTaskFunctionService.java new file mode 100644 index 0000000..779cf17 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/AgentTaskFunctionService.java @@ -0,0 +1,112 @@ +package com.will.coreService.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.will.coreData.dao.IAgentConfigGroupService; +import com.will.coreData.dao.IAgentConfigItemService; +import com.will.coreData.entity.AgentConfigGroup; +import com.will.coreData.entity.AgentConfigItem; +import com.will.coreService.dto.AgentReportJobTypeDTO; +import com.will.coreService.dto.AgentTaskFunctionDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class AgentTaskFunctionService { + + private static final String TASK_FUNCTION_GROUP_CODE = "TASK_FUNCTION"; + private static final String TASK_FUNCTION_GROUP_ID = "config-group-task-function"; + + private final IAgentConfigGroupService configGroupService; + private final IAgentConfigItemService configItemService; + + public List list(String name, String code, Boolean enabled) { + ensureTaskFunctionGroup(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(AgentConfigItem::getGroupId, TASK_FUNCTION_GROUP_ID) + .like(StringUtils.hasText(name), AgentConfigItem::getName, name == null ? null : name.trim()) + .like(StringUtils.hasText(code), AgentConfigItem::getCode, code == null ? null : code.trim()) + .eq(enabled != null, AgentConfigItem::getEnabled, enabled) + .orderByAsc(AgentConfigItem::getSortOrder) + .orderByDesc(AgentConfigItem::getLastUpdated); + return configItemService.list(queryWrapper) + .stream() + .map(this::toDTO) + .toList(); + } + + public List enabledJobTypes() { + ensureTaskFunctionGroup(); + return configItemService.list(new LambdaQueryWrapper() + .eq(AgentConfigItem::getGroupId, TASK_FUNCTION_GROUP_ID) + .eq(AgentConfigItem::getEnabled, Boolean.TRUE) + .orderByAsc(AgentConfigItem::getSortOrder) + .orderByAsc(AgentConfigItem::getCode)) + .stream() + .map(item -> new AgentReportJobTypeDTO(item.getCode(), item.getName(), item.getDescription())) + .toList(); + } + + @Transactional + public AgentTaskFunctionDTO save(AgentTaskFunctionDTO dto) { + ensureTaskFunctionGroup(); + AgentConfigItem entity = StringUtils.hasText(dto.getId()) + ? configItemService.getById(dto.getId()) + : null; + if (entity == null) { + entity = new AgentConfigItem(); + entity.setId(UUID.randomUUID().toString().replace("-", "")); + } + entity.setGroupId(TASK_FUNCTION_GROUP_ID); + entity.setCode(requiredString(dto.getCode(), "任务功能编码")); + entity.setName(requiredString(dto.getName(), "任务功能名称")); + entity.setDescription(trimToNull(dto.getDescription())); + entity.setEnabled(dto.getEnabled() == null || dto.getEnabled()); + entity.setSortOrder(dto.getSortOrder() == null ? 100 : dto.getSortOrder()); + configItemService.saveOrUpdate(entity); + return toDTO(entity); + } + + @Transactional + public void delete(String id) { + configItemService.removeById(id); + } + + private void ensureTaskFunctionGroup() { + AgentConfigGroup group = configGroupService.getById(TASK_FUNCTION_GROUP_ID); + if (group != null) { + return; + } + group = new AgentConfigGroup(); + group.setId(TASK_FUNCTION_GROUP_ID); + group.setCode(TASK_FUNCTION_GROUP_CODE); + group.setName("任务功能配置"); + group.setDescription("计划任务可选择的任务功能列表"); + group.setEnabled(true); + group.setSortOrder(100); + configGroupService.save(group); + } + + private AgentTaskFunctionDTO toDTO(AgentConfigItem entity) { + AgentTaskFunctionDTO dto = new AgentTaskFunctionDTO(); + BeanUtils.copyProperties(entity, dto); + return dto; + } + + private String requiredString(String value, String name) { + if (!StringUtils.hasText(value)) { + throw new IllegalArgumentException(name + "不能为空"); + } + return value.trim(); + } + + private String trimToNull(String value) { + return StringUtils.hasText(value) ? value.trim() : null; + } +} diff --git a/coreService/src/main/java/com/will/coreService/service/DataAuthService.java b/coreService/src/main/java/com/will/coreService/service/DataAuthService.java new file mode 100644 index 0000000..b3a5282 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/DataAuthService.java @@ -0,0 +1,286 @@ +package com.will.coreService.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.will.coreCommon.auth.DataScope; +import com.will.coreCommon.context.DataAuthContext; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreData.auth.AuthRbacSupport; +import com.will.coreData.dao.IAuthDepartmentService; +import com.will.coreData.dao.IAuthDepartmentUserService; +import com.will.coreData.dao.IAuthRoleService; +import com.will.coreData.dao.IAuthUserRoleService; +import com.will.coreData.entity.AuthDataPermission; +import com.will.coreData.entity.AuthDepartment; +import com.will.coreData.entity.AuthDepartmentUser; +import com.will.coreData.entity.AuthRole; +import com.will.coreData.entity.AuthUserRole; +import com.will.coreData.mapper.AuthDataPermissionMapper; +import com.will.coreService.dto.DataPermissionDTO; +import com.will.coreService.dto.DataPermissionListItemDTO; +import com.will.coreService.dto.DataPermissionSaveDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 数据权限服务:解析范围、过滤部门/用户、保存角色数据权限配置。 + */ +@Service +@RequiredArgsConstructor +public class DataAuthService { + + private final AuthDataPermissionMapper authDataPermissionMapper; + private final IAuthUserRoleService authUserRoleService; + private final IAuthDepartmentUserService authDepartmentUserService; + private final IAuthDepartmentService authDepartmentService; + private final IAuthRoleService authRoleService; + private final AuthRbacSupport authRbacSupport; + + public List getPermissionByRoleId(String roleId) { + if (!StringUtils.hasText(roleId)) { + return List.of(); + } + return authDataPermissionMapper.getPermissionByRoleId(roleId); + } + + public List listByRoleId(String roleId) { + return getPermissionByRoleId(roleId).stream().map(this::toDto).toList(); + } + + public DataPermissionDTO getPrimaryByRoleId(String roleId) { + List list = listByRoleId(roleId); + return list.isEmpty() ? null : list.getFirst(); + } + + public List listAll() { + return authRoleService.list().stream().map(role -> { + DataPermissionListItemDTO item = new DataPermissionListItemDTO(); + item.setRoleId(role.getId()); + item.setRoleCode(role.getCode()); + item.setRoleName(role.getName()); + List permissions = getPermissionByRoleId(role.getId()); + if (permissions.isEmpty()) { + item.setReadPermission(DataScope.SELF.name()); + item.setEditPermission(DataScope.SELF.name()); + item.setDeletePermission(DataScope.SELF.name()); + return item; + } + AuthDataPermission permission = permissions.getFirst(); + item.setId(permission.getId()); + item.setReadPermission(permission.getReadPermission()); + item.setEditPermission(permission.getEditPermission()); + item.setDeletePermission(permission.getDeletePermission()); + return item; + }).toList(); + } + + @Transactional + public ResponseMessage save(DataPermissionSaveDTO dto) { + if (!StringUtils.hasText(dto.getRoleId())) { + return ResponseMessageFactory.error("角色ID不能为空"); + } + if (!StringUtils.hasText(dto.getReadPermission())) { + return ResponseMessageFactory.error("数据范围不能为空"); + } + + DataScope scope = parseScope(dto.getReadPermission()); + AuthDataPermission entity = getPermissionByRoleId(dto.getRoleId()).stream() + .findFirst() + .orElseGet(() -> { + AuthDataPermission created = new AuthDataPermission(); + created.setId(UUID.randomUUID().toString()); + created.setRoleId(dto.getRoleId()); + return created; + }); + + String scopeName = scope.name(); + entity.setReadPermission(scopeName); + entity.setEditPermission(StringUtils.hasText(dto.getEditPermission()) ? dto.getEditPermission() : scopeName); + entity.setDeletePermission(StringUtils.hasText(dto.getDeletePermission()) ? dto.getDeletePermission() : scopeName); + + if (getPermissionByRoleId(dto.getRoleId()).isEmpty()) { + authDataPermissionMapper.insert(entity); + } else { + authDataPermissionMapper.updateById(entity); + } + return ResponseMessageFactory.success("保存成功"); + } + + public DataAuthContext resolveForCurrentUser(String userId) { + if (!StringUtils.hasText(userId)) { + return DataAuthContext.builder().scope(DataScope.SELF).build(); + } + List roleCodes = authRbacSupport.getRoleCodes(userId); + if (authRbacSupport.isSuperAdmin(roleCodes)) { + return DataAuthContext.builder().userId(userId).scope(DataScope.ALL).build(); + } + List relations = authUserRoleService.list( + new LambdaQueryWrapper().eq(AuthUserRole::getUserId, userId)); + if (relations.isEmpty()) { + return DataAuthContext.builder().userId(userId).scope(DataScope.SELF).build(); + } + AuthUserRole primary = relations.getFirst(); + return resolveContext(userId, primary.getDepartmentId(), primary.getRoleId()); + } + + public DataAuthContext resolveContext(String userId, String departmentId, String roleId) { + DataScope scope = DataScope.SELF; + List permissions = getPermissionByRoleId(roleId); + if (!permissions.isEmpty()) { + scope = parseScope(permissions.getFirst().getReadPermission()); + } + return DataAuthContext.builder() + .userId(userId) + .departmentId(departmentId) + .scope(scope) + .build(); + } + + /** + * @return null 表示不限制部门范围 + */ + public List resolveAllowedDepartmentIds(String userId) { + DataAuthContext context = resolveForCurrentUser(userId); + if (context.isUnrestricted()) { + return null; + } + return switch (context.getScope()) { + case ALL -> null; + case DEPT -> toSingleDepartmentScope(context.getDepartmentId()); + case DEPT_AND_CHILD -> getDepartmentIdsWithChildren(context.getDepartmentId()); + case SELF -> getUserDepartmentIds(userId); + }; + } + + /** + * @return null 表示可查看全部用户 + */ + public List resolveVisibleUserIds(String userId) { + DataAuthContext context = resolveForCurrentUser(userId); + if (context.isUnrestricted()) { + return null; + } + return switch (context.getScope()) { + case ALL -> null; + case SELF -> List.of(userId); + case DEPT, DEPT_AND_CHILD -> findUserIdsByDepartments(resolveAllowedDepartmentIds(userId)); + }; + } + + public boolean canAccessUser(String operatorId, String targetUserId) { + if (!StringUtils.hasText(targetUserId)) { + return false; + } + List visibleUserIds = resolveVisibleUserIds(operatorId); + return visibleUserIds == null || visibleUserIds.contains(targetUserId); + } + + public List getDepartmentIdsWithChildren(String departmentId) { + if (!StringUtils.hasText(departmentId)) { + return List.of(); + } + AuthDepartment department = authDepartmentService.getOne( + new LambdaQueryWrapper().eq(AuthDepartment::getId, departmentId)); + if (department == null) { + return List.of(departmentId); + } + List children = authDepartmentService.list( + new QueryWrapper().lambda().isNotNull(AuthDepartment::getParentId)); + List ids = new ArrayList<>(); + ids.add(department.getId()); + collectChildDepartmentIds(department, children, ids); + return ids; + } + + private List toSingleDepartmentScope(String departmentId) { + if (!StringUtils.hasText(departmentId)) { + return List.of(); + } + return List.of(departmentId); + } + + private List getUserDepartmentIds(String userId) { + List departmentIds = authUserRoleService.list( + new LambdaQueryWrapper().eq(AuthUserRole::getUserId, userId)) + .stream() + .map(AuthUserRole::getDepartmentId) + .filter(StringUtils::hasText) + .collect(Collectors.toCollection(ArrayList::new)); + + authDepartmentUserService.list( + new LambdaQueryWrapper().eq(AuthDepartmentUser::getUserId, userId)) + .stream() + .map(AuthDepartmentUser::getDepartmentId) + .filter(StringUtils::hasText) + .forEach(departmentIds::add); + + return departmentIds.stream().distinct().toList(); + } + + private List findUserIdsByDepartments(List departmentIds) { + if (departmentIds == null || departmentIds.isEmpty()) { + return List.of(); + } + Set userIds = new LinkedHashSet<>(); + authDepartmentUserService.list( + new LambdaQueryWrapper() + .in(AuthDepartmentUser::getDepartmentId, departmentIds)) + .stream() + .map(AuthDepartmentUser::getUserId) + .filter(StringUtils::hasText) + .forEach(userIds::add); + + authUserRoleService.list( + new LambdaQueryWrapper() + .in(AuthUserRole::getDepartmentId, departmentIds)) + .stream() + .map(AuthUserRole::getUserId) + .filter(StringUtils::hasText) + .forEach(userIds::add); + + return new ArrayList<>(userIds); + } + + private void collectChildDepartmentIds( + AuthDepartment parent, List childrenDepartments, List ids) { + childrenDepartments.forEach(child -> { + if (parent.getId().equalsIgnoreCase(child.getParentId())) { + ids.add(child.getId()); + collectChildDepartmentIds(child, childrenDepartments, ids); + } + }); + } + + private DataPermissionDTO toDto(AuthDataPermission entity) { + DataPermissionDTO dto = new DataPermissionDTO(); + dto.setId(entity.getId()); + dto.setPermissionId(entity.getPermissionId()); + dto.setRoleId(entity.getRoleId()); + dto.setUserId(entity.getUserId()); + dto.setReadPermission(entity.getReadPermission()); + dto.setEditPermission(entity.getEditPermission()); + dto.setDeletePermission(entity.getDeletePermission()); + return dto; + } + + private DataScope parseScope(String value) { + if (!StringUtils.hasText(value)) { + return DataScope.SELF; + } + try { + return DataScope.valueOf(value.trim().toUpperCase()); + } catch (IllegalArgumentException ex) { + return DataScope.SELF; + } + } +} diff --git a/coreService/src/main/java/com/will/coreService/service/DepartmentService.java b/coreService/src/main/java/com/will/coreService/service/DepartmentService.java new file mode 100644 index 0000000..f7edf13 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/DepartmentService.java @@ -0,0 +1,528 @@ +package com.will.coreService.service; + + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import com.will.coreCommon.BaseListDTO; + +import com.will.coreCommon.BasePage; + +import com.will.coreCommon.auth.DataScope; + +import com.will.coreCommon.context.DataAuthContext; + +import com.will.coreCommon.context.UserContext; + +import com.will.coreCommon.general.ResponseMessage; + +import com.will.coreCommon.general.ResponseMessageFactory; + +import com.will.coreData.dao.IAuthDepartmentService; + +import com.will.coreData.dao.IAuthDepartmentUserService; + +import com.will.coreData.dao.IAuthUserRoleService; + +import com.will.coreData.dao.IAuthUserService; + +import com.will.coreData.entity.AuthDepartment; + +import com.will.coreData.entity.AuthDepartmentUser; + +import com.will.coreData.entity.AuthUser; + +import com.will.coreData.entity.AuthUserRole; + +import com.will.coreService.dto.DepartmentDTO; + +import com.will.coreService.dto.UserDTO; + +import lombok.RequiredArgsConstructor; + +import org.springframework.beans.BeanUtils; + +import org.springframework.stereotype.Service; + +import org.springframework.transaction.annotation.Transactional; + +import org.springframework.util.StringUtils; + + + +import java.time.LocalDateTime; + +import java.util.ArrayList; + +import java.util.List; + +import java.util.Set; + +import java.util.UUID; + +import java.util.stream.Collectors; + + + +@RequiredArgsConstructor + +@Service + +public class DepartmentService { + + + + private final IAuthDepartmentService authDepartmentService; + + private final IAuthDepartmentUserService authDepartmentUserService; + + private final IAuthUserRoleService authUserRoleService; + + private final IAuthUserService authUserService; + + private final DataAuthService dataAuthService; + + + + public ResponseMessage> getDepartmentPageDto( + + DepartmentDTO condition, Integer pageIndex, Integer pageSize) { + + List scopedDepartments = listScopedDepartments(); + + List filtered = filterByName(scopedDepartments, condition); + + + + int from = Math.max(0, (pageIndex - 1) * pageSize); + + int to = Math.min(filtered.size(), from + pageSize); + + List pageRecords = from >= filtered.size() + + ? List.of() + + : filtered.subList(from, to); + + + + List departmentDtos = buildFilteredTree(pageRecords, scopedDepartments); + + BasePage basePage = new BasePage<>(departmentDtos, (long) filtered.size()); + + return ResponseMessageFactory.success(basePage); + + } + + + + public ResponseMessage> getDepartmentListDto() { + + List scopedDepartments = listScopedDepartments(); + + List departmentDtos = buildFilteredTree(scopedDepartments, scopedDepartments); + + BaseListDTO baseListDto = new BaseListDTO<>(); + + baseListDto.setList(departmentDtos); + + return ResponseMessageFactory.success(baseListDto); + + } + + + + public ResponseMessage> getDepartmentDTOs( + + DepartmentDTO condition, Integer pageIndex, Integer pageSize) { + + ResponseMessage> pageResult = + + getDepartmentPageDto(condition, pageIndex, pageSize); + + if (pageResult.hasError()) { + + return ResponseMessageFactory.error(pageResult.getMessage()); + + } + + return ResponseMessageFactory.success(pageResult.getData().getList()); + + } + + + + private List listScopedDepartments() { + + List allDepartments = authDepartmentService.list(); + + List allowedIds = dataAuthService.resolveAllowedDepartmentIds(UserContext.getCurrentUserId()); + + if (allowedIds == null) { + + return allDepartments; + + } + + Set allowedSet = Set.copyOf(allowedIds); + + return allDepartments.stream() + + .filter(department -> allowedSet.contains(department.getId())) + + .toList(); + + } + + + + private List filterByName(List departments, DepartmentDTO condition) { + + if (condition == null || !StringUtils.hasText(condition.getDepartmentName())) { + + return departments; + + } + + return departments.stream() + + .filter(department -> department.getName() != null + + && department.getName().startsWith(condition.getDepartmentName())) + + .toList(); + + } + + + + private List buildFilteredTree( + + List visibleDepartments, List scopedDepartments) { + + Set scopedIds = scopedDepartments.stream() + + .map(AuthDepartment::getId) + + .collect(Collectors.toSet()); + + return visibleDepartments.stream() + + .filter(department -> isRootWithinScope(department, scopedIds)) + + .map(department -> buildDepartmentNode(department, scopedDepartments, scopedIds)) + + .toList(); + + } + + + + private boolean isRootWithinScope(AuthDepartment department, Set scopedIds) { + + return !StringUtils.hasText(department.getParentId()) + + || department.getParentId().isEmpty() + + || !scopedIds.contains(department.getParentId()); + + } + + + + private DepartmentDTO buildDepartmentNode( + + AuthDepartment department, List scopedDepartments, Set scopedIds) { + + DepartmentDTO dto = toDepartmentDTO(department); + + List children = scopedDepartments.stream() + + .filter(item -> department.getId().equals(item.getParentId())) + + .map(item -> buildDepartmentNode(item, scopedDepartments, scopedIds)) + + .toList(); + + if (!children.isEmpty()) { + + dto.setChildren(children); + + } + + return dto; + + } + + + + private DepartmentDTO toDepartmentDTO(AuthDepartment department) { + + DepartmentDTO departmentDto = new DepartmentDTO(); + + departmentDto.setId(department.getId()); + + departmentDto.setDepartmentName(department.getName()); + + departmentDto.setCreated(formatDateTime(department.getCreated())); + + departmentDto.setStatus(department.getStatus()); + + departmentDto.setCreatedBy(department.getCreatedBy()); + + departmentDto.setDepartmentCode(department.getCode()); + + departmentDto.setParentId(department.getParentId()); + + departmentDto.setLastUpdated(formatDateTime(department.getLastUpdated())); + + departmentDto.setLastUpdatedBy(department.getLastUpdatedBy()); + + return departmentDto; + + } + + + + private String formatDateTime(LocalDateTime dateTime) { + + return dateTime == null ? null : dateTime.toString(); + + } + + + + public List getChildrenDepartmentIds(String departmentId) { + return dataAuthService.getDepartmentIdsWithChildren(departmentId); + } + + public ResponseMessage> getPageUsersByDepartmentId( + + String id, Integer pageIndex, Integer pageSize) { + + List allowedIds = dataAuthService.resolveAllowedDepartmentIds(UserContext.getCurrentUserId()); + + if (allowedIds != null && !allowedIds.contains(id)) { + + return ResponseMessageFactory.success(new BasePage<>(List.of(), 0L)); + + } + + + + List departmentIds = getChildrenDepartmentIds(id); + + if (allowedIds != null) { + + Set allowedSet = Set.copyOf(allowedIds); + + departmentIds = departmentIds.stream().filter(allowedSet::contains).toList(); + + } + + if (departmentIds.isEmpty()) { + + return ResponseMessageFactory.success(new BasePage<>(List.of(), 0L)); + + } + + + + Page relationPage = new Page<>(pageIndex, pageSize); + + LambdaQueryWrapper relationWrapper = new LambdaQueryWrapper<>(); + + relationWrapper.in(AuthDepartmentUser::getDepartmentId, departmentIds); + + relationPage = authDepartmentUserService.page(relationPage, relationWrapper); + + + + List userIds = relationPage.getRecords().stream() + + .map(AuthDepartmentUser::getUserId) + + .distinct() + + .toList(); + + if (userIds.isEmpty()) { + + return ResponseMessageFactory.success(new BasePage<>(List.of(), relationPage.getTotal())); + + } + + + + List users = authUserService.listByIds(userIds); + + List userDTOs = users.stream().map(this::toUserDTO).toList(); + + return ResponseMessageFactory.success(new BasePage<>(userDTOs, relationPage.getTotal())); + + } + + + + private UserDTO toUserDTO(AuthUser user) { + + UserDTO userDTO = new UserDTO(); + + BeanUtils.copyProperties(user, userDTO); + + return userDTO; + + } + + + + @Transactional + public boolean removeBatchByIds(List ids) { + + if (ids == null || ids.isEmpty()) { + + return false; + + } + + List allowedIds = dataAuthService.resolveAllowedDepartmentIds(UserContext.getCurrentUserId()); + + if (allowedIds != null) { + + Set allowedSet = Set.copyOf(allowedIds); + + ids = ids.stream().filter(allowedSet::contains).toList(); + + if (ids.isEmpty()) { + + return false; + + } + + } + + List deleteIds = ids.stream() + .flatMap(id -> getChildrenDepartmentIds(id).stream()) + .distinct() + .toList(); + if (deleteIds.isEmpty()) { + return false; + } + + authDepartmentUserService.remove( + new LambdaQueryWrapper().in(AuthDepartmentUser::getDepartmentId, deleteIds)); + authUserRoleService.remove( + new LambdaQueryWrapper().in(AuthUserRole::getDepartmentId, deleteIds)); + return authDepartmentService.removeByIds(deleteIds); + + } + + + + public ResponseMessage saveDepartment(DepartmentDTO departmentDto) { + + if (departmentDto == null || !StringUtils.hasText(departmentDto.getDepartmentName())) { + + return ResponseMessageFactory.error("部门名称不能为空"); + + } + + + + List allowedIds = dataAuthService.resolveAllowedDepartmentIds(UserContext.getCurrentUserId()); + + if (allowedIds != null && StringUtils.hasText(departmentDto.getParentId())) { + + if (!allowedIds.contains(departmentDto.getParentId())) { + + return ResponseMessageFactory.error("无权在该上级部门下创建部门"); + + } + + } + + + + AuthDepartment authDepartment = new AuthDepartment(); + + authDepartment.setId(departmentDto.getId()); + + authDepartment.setName(departmentDto.getDepartmentName()); + + authDepartment.setCode(departmentDto.getDepartmentCode()); + + authDepartment.setParentId(departmentDto.getParentId() == null ? "" : departmentDto.getParentId()); + + authDepartment.setStatus(departmentDto.getStatus() == null ? 1 : departmentDto.getStatus()); + + + + AuthDepartment existing = null; + + if (StringUtils.hasText(authDepartment.getId())) { + + existing = authDepartmentService.getOne( + + new LambdaQueryWrapper().eq(AuthDepartment::getId, authDepartment.getId())); + + if (allowedIds != null && existing != null && !allowedIds.contains(existing.getId())) { + + return ResponseMessageFactory.error("无权修改该部门"); + + } + + if (existing != null && existing.getId().equals(authDepartment.getParentId())) { + return ResponseMessageFactory.error("上级部门不能选择自身"); + } + + if (existing != null && getChildrenDepartmentIds(existing.getId()).contains(authDepartment.getParentId())) { + return ResponseMessageFactory.error("上级部门不能选择下级部门"); + } + + } + + if (existsSameDepartment(authDepartment)) { + return ResponseMessageFactory.error("同级部门名称或编码已存在"); + } + + + + if (existing != null) { + + authDepartment.setId(existing.getId()); + + authDepartmentService.updateById(authDepartment); + + } else { + + if (!StringUtils.hasText(authDepartment.getId())) { + + authDepartment.setId(UUID.randomUUID().toString()); + + } + + authDepartmentService.save(authDepartment); + + } + + return ResponseMessageFactory.success("保存成功"); + + } + + private boolean existsSameDepartment(AuthDepartment department) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AuthDepartment::getParentId, department.getParentId()); + wrapper.and(condition -> condition.eq(AuthDepartment::getName, department.getName()) + .or(StringUtils.hasText(department.getCode()), codeCondition -> codeCondition.eq(AuthDepartment::getCode, department.getCode()))); + if (StringUtils.hasText(department.getId())) { + wrapper.ne(AuthDepartment::getId, department.getId()); + } + return authDepartmentService.count(wrapper) > 0; + } + +} + diff --git a/coreService/src/main/java/com/will/coreService/service/MenuManageService.java b/coreService/src/main/java/com/will/coreService/service/MenuManageService.java new file mode 100644 index 0000000..1d3a27e --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/MenuManageService.java @@ -0,0 +1,479 @@ +package com.will.coreService.service; + + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; + +import com.will.coreCommon.BaseListDTO; + +import com.will.coreCommon.auth.MenuTypes; + +import com.will.coreCommon.general.ResponseMessage; + +import com.will.coreCommon.general.ResponseMessageFactory; + +import com.will.coreData.auth.AuthRbacSupport; + +import com.will.coreData.dao.IAuthMenuService; + +import com.will.coreData.entity.AuthMenu; + +import com.will.coreService.dto.MenuDTO; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +import org.springframework.util.StringUtils; + + + +import java.util.ArrayList; + +import java.util.Comparator; + +import java.util.HashMap; + +import java.util.LinkedHashMap; + +import java.util.List; + +import java.util.Map; + +import java.util.Set; + +import java.util.UUID; + + + +@Service + +@RequiredArgsConstructor + +public class MenuManageService { + + + + private final IAuthMenuService authMenuService; + + private final AuthRbacSupport authRbacSupport; + + + + public ResponseMessage> listTree() { + + List allMenus = authMenuService.list( + + new LambdaQueryWrapper().orderByAsc(AuthMenu::getSortOrder)); + + List roots = allMenus.stream() + + .filter(menu -> !StringUtils.hasText(menu.getParentId())) + + .toList(); + + List tree = roots.stream().map(root -> buildMenuTree(root, allMenus)).toList(); + + BaseListDTO result = new BaseListDTO<>(); + + result.setList(tree); + + return ResponseMessageFactory.success(result); + + } + + + + public ResponseMessage save(MenuDTO dto) { + + if (!StringUtils.hasText(dto.getCode()) || !StringUtils.hasText(dto.getName())) { + + return ResponseMessageFactory.error("菜单编码和名称不能为空"); + + } + + + + AuthMenu menu = new AuthMenu(); + + if (StringUtils.hasText(dto.getId())) { + + AuthMenu existing = authMenuService.getById(dto.getId()); + + if (existing == null) { + + return ResponseMessageFactory.error("菜单不存在"); + + } + + menu.setId(existing.getId()); + + } else { + + menu.setId(UUID.randomUUID().toString()); + + } + + menu.setCode(dto.getCode()); + + menu.setName(dto.getName()); + + menu.setParentId(StringUtils.hasText(dto.getParentId()) ? dto.getParentId() : null); + + menu.setMenuType(StringUtils.hasText(dto.getMenuType()) ? dto.getMenuType() : MenuTypes.PERM); + + menu.setRoutePath(dto.getRoutePath()); + + menu.setRouteName(dto.getRouteName()); + + menu.setComponent(dto.getComponent()); + + menu.setRedirect(dto.getRedirect()); + + menu.setIcon(dto.getIcon()); + + menu.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0); + + menu.setTitle(dto.getTitle()); + + + + if (StringUtils.hasText(dto.getId())) { + + authMenuService.updateById(menu); + + } else { + + authMenuService.save(menu); + + } + + return ResponseMessageFactory.success("保存成功"); + + } + + + + public boolean removeBatchByIds(List ids) { + + if (ids == null || ids.isEmpty()) { + + return false; + + } + + return authMenuService.removeByIds(ids); + + } + + + + /** + + * 从 auth_menu 构建 vben-admin 动态路由(按用户权限过滤)。 + + */ + + public List> getUserMenus(String userId) { + + Set permissions = Set.copyOf(authRbacSupport.getPermissionCodes(userId)); + + boolean superAdmin = authRbacSupport.isSuperAdmin(authRbacSupport.getRoleCodes(userId)); + + + + List routeMenus = authMenuService.list( + + new LambdaQueryWrapper() + + .in(AuthMenu::getMenuType, MenuTypes.DIR, MenuTypes.MENU) + + .orderByAsc(AuthMenu::getSortOrder)); + + + + List visibleMenus = routeMenus.stream() + + .filter(menu -> isRouteVisible(menu, permissions, superAdmin)) + + .toList(); + + + + List> roots = buildRouteTree(null, visibleMenus); + + return pruneEmptyDirs(roots); + + } + + + + private boolean isRouteVisible(AuthMenu menu, Set permissions, boolean superAdmin) { + + if (MenuTypes.DIR.equals(menu.getMenuType())) { + + return isPublicRoute(menu) || superAdmin || hasVisibleDescendant(menu, permissions, superAdmin); + + } + + if (superAdmin) { + + return true; + + } + + return permissions.contains(menu.getCode()); + + } + + + + private boolean isPublicRoute(AuthMenu menu) { + + return StringUtils.hasText(menu.getCode()) && menu.getCode().startsWith(MenuTypes.ROUTE_CODE_PREFIX); + + } + + + + private boolean hasVisibleDescendant(AuthMenu parent, Set permissions, boolean superAdmin) { + + List children = authMenuService.list( + + new LambdaQueryWrapper().eq(AuthMenu::getParentId, parent.getId())); + + for (AuthMenu child : children) { + + if (MenuTypes.PERM.equals(child.getMenuType())) { + + continue; + + } + + if (isRouteVisible(child, permissions, superAdmin)) { + + return true; + + } + + } + + return false; + + } + + + + private List> buildRouteTree(String parentId, List visibleMenus) { + + return visibleMenus.stream() + + .filter(menu -> { + + if (!StringUtils.hasText(parentId)) { + + return !StringUtils.hasText(menu.getParentId()); + + } + + return parentId.equals(menu.getParentId()); + + }) + + .sorted(Comparator.comparing(menu -> menu.getSortOrder() != null ? menu.getSortOrder() : 0)) + + .map(menu -> toRouteNode(menu, visibleMenus)) + + .toList(); + + } + + + + private Map toRouteNode(AuthMenu menu, List visibleMenus) { + + Map route = new LinkedHashMap<>(); + + route.put("name", menu.getRouteName()); + + route.put("path", menu.getRoutePath()); + + if (StringUtils.hasText(menu.getComponent())) { + + route.put("component", menu.getComponent()); + + } + + if (StringUtils.hasText(menu.getRedirect())) { + + route.put("redirect", menu.getRedirect()); + + } + + + + Map meta = new HashMap<>(); + + if (StringUtils.hasText(menu.getTitle())) { + + meta.put("title", menu.getTitle()); + + } else { + + meta.put("title", menu.getName()); + + } + + if (StringUtils.hasText(menu.getIcon())) { + + meta.put("icon", menu.getIcon()); + + } + + if (menu.getSortOrder() != null) { + + meta.put("order", menu.getSortOrder()); + + } + + if (StringUtils.hasText(menu.getCode()) && !isPublicRoute(menu)) { + + meta.put("authority", List.of(menu.getCode())); + + } + + route.put("meta", meta); + + + + List> children = buildRouteTree(menu.getId(), visibleMenus); + + if (!children.isEmpty()) { + + route.put("children", children); + + } + + return route; + + } + + + + private List> pruneEmptyDirs(List> routes) { + + List> result = new ArrayList<>(); + + for (Map route : routes) { + + Object childrenObj = route.get("children"); + + if (childrenObj instanceof List children) { + + @SuppressWarnings("unchecked") + + List> prunedChildren = pruneEmptyDirs((List>) children); + + if (prunedChildren.isEmpty() && route.get("component") == null) { + + continue; + + } + + route.put("children", prunedChildren); + + } + + if (route.get("component") != null || route.get("children") != null) { + + result.add(route); + + } + + } + + return result; + + } + + + + private MenuDTO buildMenuTree(AuthMenu menu, List allMenus) { + + MenuDTO dto = new MenuDTO(); + + dto.setId(menu.getId()); + + dto.setCode(menu.getCode()); + + dto.setName(menu.getName()); + + dto.setParentId(menu.getParentId()); + + dto.setMenuType(menu.getMenuType()); + + dto.setRoutePath(menu.getRoutePath()); + + dto.setRouteName(menu.getRouteName()); + + dto.setComponent(menu.getComponent()); + + dto.setRedirect(menu.getRedirect()); + + dto.setIcon(menu.getIcon()); + + dto.setSortOrder(menu.getSortOrder()); + + dto.setTitle(menu.getTitle()); + + + + List children = allMenus.stream() + + .filter(item -> menu.getId().equals(resolveParentId(item))) + + .map(child -> buildMenuTree(child, allMenus)) + + .toList(); + + if (!children.isEmpty()) { + + dto.setChildren(children); + + } + + return dto; + + } + + private String resolveParentId(AuthMenu menu) { + if (StringUtils.hasText(menu.getParentId())) { + return menu.getParentId(); + } + String code = menu.getCode(); + if (!StringUtils.hasText(code) || !MenuTypes.PERM.equals(menu.getMenuType())) { + return null; + } + if (code.startsWith("sys:department:")) { + return "menu-dept-page"; + } + if (code.startsWith("sys:user:")) { + return "menu-user-page"; + } + if (code.startsWith("sys:role:")) { + return "menu-role-page"; + } + if (code.startsWith("sys:menu:")) { + return "menu-menu-page"; + } + if (code.startsWith("sys:data-permission:")) { + return "menu-data-perm-page"; + } + if (code.startsWith("sys:agent:")) { + return "menu-agent-page"; + } + return null; + } + +} + diff --git a/coreService/src/main/java/com/will/coreService/service/RoleService.java b/coreService/src/main/java/com/will/coreService/service/RoleService.java new file mode 100644 index 0000000..03adee0 --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/RoleService.java @@ -0,0 +1,136 @@ +package com.will.coreService.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.will.coreCommon.BaseListDTO; +import com.will.coreCommon.general.ResponseMessage; +import com.will.coreCommon.general.ResponseMessageFactory; +import com.will.coreData.dao.IAuthRoleMenuService; +import com.will.coreData.dao.IAuthRoleService; +import com.will.coreData.dao.IAuthUserRoleService; +import com.will.coreData.entity.AuthRole; +import com.will.coreData.entity.AuthRoleMenu; +import com.will.coreData.entity.AuthUserRole; +import com.will.coreService.dto.RoleDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class RoleService { + + private final IAuthRoleService authRoleService; + private final IAuthRoleMenuService authRoleMenuService; + private final IAuthUserRoleService authUserRoleService; + + public ResponseMessage> list() { + List roles = authRoleService.list().stream().map(this::toRoleDTO).toList(); + BaseListDTO result = new BaseListDTO<>(); + result.setList(roles); + return ResponseMessageFactory.success(result); + } + + public ResponseMessage detail(String id) { + AuthRole role = authRoleService.getById(id); + if (role == null) { + return ResponseMessageFactory.error("角色不存在"); + } + RoleDTO dto = toRoleDTO(role); + dto.setMenuIds(getMenuIds(id)); + return ResponseMessageFactory.success(dto); + } + + @Transactional + public ResponseMessage save(RoleDTO dto) { + if (!StringUtils.hasText(dto.getCode()) || !StringUtils.hasText(dto.getName())) { + return ResponseMessageFactory.error("角色编码和名称不能为空"); + } + + AuthRole role = new AuthRole(); + AuthRole duplicate = authRoleService.getOne( + new LambdaQueryWrapper().eq(AuthRole::getCode, dto.getCode()), false); + if (duplicate != null && !duplicate.getId().equals(dto.getId())) { + return ResponseMessageFactory.error("角色编码已存在"); + } + + if (StringUtils.hasText(dto.getId())) { + AuthRole existing = authRoleService.getById(dto.getId()); + if (existing == null) { + return ResponseMessageFactory.error("角色不存在"); + } + role.setId(existing.getId()); + } else { + role.setId(UUID.randomUUID().toString()); + } + role.setCode(dto.getCode()); + role.setName(dto.getName()); + role.setDepartmentId(dto.getDepartmentId()); + + if (StringUtils.hasText(dto.getId())) { + authRoleService.updateById(role); + } else { + authRoleService.save(role); + } + + if (dto.getMenuIds() != null) { + saveRoleMenus(role.getId(), dto.getMenuIds()); + } + return ResponseMessageFactory.success(role.getId()); + } + + @Transactional + public ResponseMessage assignMenus(String roleId, List menuIds) { + AuthRole role = authRoleService.getById(roleId); + if (role == null) { + return ResponseMessageFactory.error("角色不存在"); + } + saveRoleMenus(roleId, menuIds); + return ResponseMessageFactory.success("分配成功"); + } + + @Transactional + public boolean removeBatchByIds(List ids) { + if (ids == null || ids.isEmpty()) { + return false; + } + authRoleMenuService.remove(new LambdaQueryWrapper().in(AuthRoleMenu::getRoleId, ids)); + authUserRoleService.remove(new LambdaQueryWrapper().in(AuthUserRole::getRoleId, ids)); + return authRoleService.removeByIds(ids); + } + + private void saveRoleMenus(String roleId, List menuIds) { + authRoleMenuService.remove( + new LambdaQueryWrapper().eq(AuthRoleMenu::getRoleId, roleId)); + if (menuIds == null || menuIds.isEmpty()) { + return; + } + menuIds.stream().filter(StringUtils::hasText).distinct().forEach(menuId -> { + AuthRoleMenu relation = new AuthRoleMenu(); + relation.setId(UUID.randomUUID().toString()); + relation.setRoleId(roleId); + relation.setMenuId(menuId); + authRoleMenuService.save(relation); + }); + } + + private List getMenuIds(String roleId) { + return authRoleMenuService.list( + new LambdaQueryWrapper().eq(AuthRoleMenu::getRoleId, roleId)) + .stream() + .map(AuthRoleMenu::getMenuId) + .toList(); + } + + private RoleDTO toRoleDTO(AuthRole role) { + RoleDTO dto = new RoleDTO(); + dto.setId(role.getId()); + dto.setCode(role.getCode()); + dto.setName(role.getName()); + dto.setDepartmentId(role.getDepartmentId()); + return dto; + } +} diff --git a/coreService/src/main/java/com/will/coreService/service/UserService.java b/coreService/src/main/java/com/will/coreService/service/UserService.java new file mode 100644 index 0000000..cff2f2a --- /dev/null +++ b/coreService/src/main/java/com/will/coreService/service/UserService.java @@ -0,0 +1,450 @@ +package com.will.coreService.service; + + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import com.will.coreCommon.BasePage; + +import com.will.coreCommon.context.UserContext; + +import com.will.coreCommon.general.ResponseMessage; + +import com.will.coreCommon.general.ResponseMessageFactory; + +import com.will.coreCommon.util.Coder; + +import com.will.coreData.dao.IAuthDepartmentUserService; + +import com.will.coreData.dao.IAuthUserPasswordService; + +import com.will.coreData.dao.IAuthUserRoleService; + +import com.will.coreData.dao.IAuthUserSaltService; + +import com.will.coreData.dao.IAuthUserService; + +import com.will.coreData.entity.AuthDepartmentUser; + +import com.will.coreData.entity.AuthUser; + +import com.will.coreData.entity.AuthUserPassword; + +import com.will.coreData.entity.AuthUserRole; + +import com.will.coreData.entity.AuthUserSalt; + +import com.will.coreService.dto.UserDTO; + +import com.will.coreService.dto.UserSaveDTO; + +import lombok.RequiredArgsConstructor; + +import org.springframework.beans.BeanUtils; + +import org.springframework.stereotype.Service; + +import org.springframework.transaction.annotation.Transactional; + +import org.springframework.util.StringUtils; + + + +import java.nio.charset.StandardCharsets; + +import java.util.List; + +import java.util.UUID; + + + +@Service + +@RequiredArgsConstructor + +public class UserService { + + + + private final IAuthUserService authUserService; + + private final IAuthDepartmentUserService authDepartmentUserService; + + private final IAuthUserRoleService authUserRoleService; + + private final IAuthUserPasswordService authUserPasswordService; + + private final IAuthUserSaltService authUserSaltService; + + private final DataAuthService dataAuthService; + + + + public ResponseMessage> page(String account, Integer pageIndex, Integer pageSize) { + + List visibleUserIds = dataAuthService.resolveVisibleUserIds(UserContext.getCurrentUserId()); + + if (visibleUserIds != null && visibleUserIds.isEmpty()) { + + return ResponseMessageFactory.success(new BasePage<>(List.of(), 0L)); + + } + + + + Page page = new Page<>(pageIndex, pageSize); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(account)) { + + String keyword = account.trim(); + wrapper.and(w -> w.like(AuthUser::getAccount, keyword) + .or() + .like(AuthUser::getUserName, keyword)); + + } + + if (visibleUserIds != null) { + + wrapper.in(AuthUser::getId, visibleUserIds); + + } + + authUserService.page(page, wrapper); + + + + List list = page.getRecords().stream().map(this::toUserDTO).toList(); + + return ResponseMessageFactory.success(new BasePage<>(list, page.getTotal())); + + } + + + + public ResponseMessage detail(String id) { + + if (!dataAuthService.canAccessUser(UserContext.getCurrentUserId(), id)) { + + return ResponseMessageFactory.error("无权查看该用户"); + + } + + AuthUser user = authUserService.getById(id); + + if (user == null) { + + return ResponseMessageFactory.error("用户不存在"); + + } + + UserSaveDTO dto = new UserSaveDTO(); + + BeanUtils.copyProperties(user, dto); + + dto.setDepartmentIds(getDepartmentIds(id)); + + dto.setRoleIds(getRoleIds(id)); + + return ResponseMessageFactory.success(dto); + + } + + + + @Transactional + + public ResponseMessage save(UserSaveDTO dto) { + + if (!StringUtils.hasText(dto.getAccount())) { + + return ResponseMessageFactory.error("账号不能为空"); + + } + + + + AuthUser existing = null; + + if (StringUtils.hasText(dto.getId())) { + + existing = authUserService.getById(dto.getId()); + + if (existing == null) { + return ResponseMessageFactory.error("用户不存在"); + } + + if (existing != null && !dataAuthService.canAccessUser(UserContext.getCurrentUserId(), existing.getId())) { + + return ResponseMessageFactory.error("无权修改该用户"); + + } + + } + + AuthUser duplicate = authUserService.findByAccount(dto.getAccount()); + if (duplicate != null && (existing == null || !duplicate.getId().equals(existing.getId()))) { + return ResponseMessageFactory.error("账号已存在"); + } + + List departmentIds = normalizeIds(dto.getDepartmentIds()); + if (!canUseDepartments(departmentIds)) { + return ResponseMessageFactory.error("无权选择该部门"); + } + + + + AuthUser user = new AuthUser(); + + if (existing != null) { + + user.setId(existing.getId()); + + } else { + + user.setId(UUID.randomUUID().toString()); + + } + + user.setAccount(dto.getAccount()); + + user.setUserName(dto.getUserName()); + + user.setEmail(dto.getEmail()); + + user.setPhoneNumber(dto.getPhoneNumber()); + + user.setStatus(dto.getStatus() == null ? Boolean.TRUE : dto.getStatus()); + + + + if (existing != null) { + + authUserService.updateById(user); + + } else { + + authUserService.save(user); + + savePassword(user.getId(), dto.getPassword() == null ? "123456" : dto.getPassword()); + + } + + + + if (StringUtils.hasText(dto.getPassword()) && existing != null) { + + savePassword(user.getId(), dto.getPassword()); + + } + + + + saveUserDepartments(user.getId(), departmentIds); + + saveUserRoles(user.getId(), dto.getRoleIds(), primaryDepartmentId(departmentIds)); + + return ResponseMessageFactory.success(user.getId()); + + } + + + + @Transactional + + public boolean removeBatchByIds(List ids) { + + if (ids == null || ids.isEmpty()) { + + return false; + + } + + String operatorId = UserContext.getCurrentUserId(); + + List deletableIds = ids.stream() + + .filter(id -> dataAuthService.canAccessUser(operatorId, id)) + + .toList(); + + if (deletableIds.isEmpty()) { + + return false; + + } + + authUserRoleService.remove(new LambdaQueryWrapper().in(AuthUserRole::getUserId, deletableIds)); + + authDepartmentUserService.remove(new LambdaQueryWrapper().in(AuthDepartmentUser::getUserId, deletableIds)); + + authUserPasswordService.remove(new LambdaQueryWrapper().in(AuthUserPassword::getUserId, deletableIds)); + + authUserSaltService.remove(new LambdaQueryWrapper().in(AuthUserSalt::getUserId, deletableIds)); + + return authUserService.removeByIds(deletableIds); + + } + + + + private void savePassword(String userId, String rawPassword) { + + authUserPasswordService.remove( + + new LambdaQueryWrapper().eq(AuthUserPassword::getUserId, userId)); + + authUserSaltService.remove( + + new LambdaQueryWrapper().eq(AuthUserSalt::getUserId, userId)); + + + + String salt = UUID.randomUUID().toString().replace("-", ""); + + AuthUserSalt userSalt = new AuthUserSalt(); + + userSalt.setId(UUID.randomUUID().toString()); + + userSalt.setUserId(userId); + + userSalt.setSalt(salt); + + authUserSaltService.save(userSalt); + + + + String encoded; + + try { + + encoded = Coder.encoded256Password((rawPassword + salt).getBytes(StandardCharsets.UTF_8)); + + } catch (Exception e) { + + encoded = rawPassword; + + } + + + + AuthUserPassword password = new AuthUserPassword(); + + password.setId(UUID.randomUUID().toString()); + + password.setUserId(userId); + + password.setPassword(encoded); + + authUserPasswordService.save(password); + + } + + + + private void saveUserDepartments(String userId, List departmentIds) { + authDepartmentUserService.remove( + new LambdaQueryWrapper().eq(AuthDepartmentUser::getUserId, userId)); + if (departmentIds.isEmpty()) { + return; + } + departmentIds.forEach(departmentId -> { + AuthDepartmentUser relation = new AuthDepartmentUser(); + relation.setId(UUID.randomUUID().toString()); + relation.setUserId(userId); + relation.setDepartmentId(departmentId); + authDepartmentUserService.save(relation); + }); + } + + private void saveUserRoles(String userId, List roleIds, String departmentId) { + + authUserRoleService.remove( + + new LambdaQueryWrapper().eq(AuthUserRole::getUserId, userId)); + + if (roleIds == null || roleIds.isEmpty()) { + + return; + + } + + roleIds.stream().filter(StringUtils::hasText).distinct().forEach(roleId -> { + + AuthUserRole relation = new AuthUserRole(); + + relation.setId(UUID.randomUUID().toString()); + + relation.setUserId(userId); + + relation.setRoleId(roleId); + relation.setDepartmentId(departmentId); + + authUserRoleService.save(relation); + + }); + + } + + + + private List getRoleIds(String userId) { + + return authUserRoleService.list( + + new LambdaQueryWrapper().eq(AuthUserRole::getUserId, userId)) + + .stream() + + .map(AuthUserRole::getRoleId) + + .toList(); + + } + + private List getDepartmentIds(String userId) { + return authDepartmentUserService.list( + new LambdaQueryWrapper().eq(AuthDepartmentUser::getUserId, userId)) + .stream() + .map(AuthDepartmentUser::getDepartmentId) + .filter(StringUtils::hasText) + .distinct() + .toList(); + } + + private boolean canUseDepartments(List departmentIds) { + if (departmentIds.isEmpty()) { + return true; + } + List allowedIds = dataAuthService.resolveAllowedDepartmentIds(UserContext.getCurrentUserId()); + return allowedIds == null || allowedIds.containsAll(departmentIds); + } + + private List normalizeIds(List ids) { + if (ids == null || ids.isEmpty()) { + return List.of(); + } + return ids.stream().filter(StringUtils::hasText).distinct().toList(); + } + + private String primaryDepartmentId(List departmentIds) { + return departmentIds.isEmpty() ? null : departmentIds.getFirst(); + } + + + + private UserDTO toUserDTO(AuthUser user) { + + UserDTO dto = new UserDTO(); + + BeanUtils.copyProperties(user, dto); + + return dto; + + } + +} + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6e13d4e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() // 必须包含!插件托管在此 + } + plugins { + // ✅ 使用真实存在的版本 + id 'org.springframework.boot' version '3.5.7' + id 'io.spring.dependency-management' version '1.1.7' + } +} +rootProject.name = 'willSystem' +include 'wcsApp' +include 'coreData' +include 'coreAuth' +include 'coreQueue' +include 'coreCommon' +include 'wcsService' +include 'coreApi' +include 'coreService' +include 'coreRedis' +include 'coreAgent' +include 'coreJob' +include 'businessJob' +include 'businessAgent' diff --git a/wcsApp/build.gradle b/wcsApp/build.gradle new file mode 100644 index 0000000..b76ad69 --- /dev/null +++ b/wcsApp/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'org.springframework.boot' + id 'io.spring.dependency-management' +} + +bootJar { + mainClass = 'com.will.wcsApp.WcsAppApplication' +} + +dependencies { + implementation project(':coreApi') + implementation project(':coreData') + implementation project(':coreAuth') + implementation project(':coreAgent') + implementation project(':businessAgent') + implementation project(':coreJob') + implementation("org.springframework.boot:spring-boot-starter-web") + + implementation("com.baomidou:mybatis-plus-spring-boot3-starter:3.5.14") +} diff --git a/wcsApp/src/main/java/com/will/wcsApp/WcsAppApplication.java b/wcsApp/src/main/java/com/will/wcsApp/WcsAppApplication.java new file mode 100644 index 0000000..0f5f4b8 --- /dev/null +++ b/wcsApp/src/main/java/com/will/wcsApp/WcsAppApplication.java @@ -0,0 +1,17 @@ +package com.will.wcsApp; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan(basePackages = {"com.will"}) +@MapperScan(basePackages = {"com.will"},markerInterface = BaseMapper.class) +public class WcsAppApplication { + public static void main(String[] args) { + SpringApplication.run(WcsAppApplication.class, args); + + } +} diff --git a/wcsApp/src/main/java/com/will/wcsApp/config/PrimaryDataSourceConfiguration.java b/wcsApp/src/main/java/com/will/wcsApp/config/PrimaryDataSourceConfiguration.java new file mode 100644 index 0000000..a2255c5 --- /dev/null +++ b/wcsApp/src/main/java/com/will/wcsApp/config/PrimaryDataSourceConfiguration.java @@ -0,0 +1,30 @@ +package com.will.wcsApp.config; + +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; + +@Configuration +public class PrimaryDataSourceConfiguration { + + @Bean + @Primary + @ConfigurationProperties("spring.datasource") + public DataSource dataSource() { + return DataSourceBuilder.create() + .type(HikariDataSource.class) + .build(); + } + + @Bean + @Primary + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } +} diff --git a/wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java b/wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java new file mode 100644 index 0000000..83c7163 --- /dev/null +++ b/wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java @@ -0,0 +1,384 @@ +package com.will.wcsApp.startup; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * 启动时补齐增量 RBAC 菜单(幂等),避免已有库未手工执行 patch SQL 时菜单缺失。 + */ +@Slf4j +@Component +@Order(100) +@RequiredArgsConstructor +public class RbacMenuPatchRunner implements ApplicationRunner { + + private final JdbcTemplate jdbcTemplate; + + @Override + public void run(ApplicationArguments args) { + patchAgentChatHistoryTables(); + patchSqlResource("/db/patch_agent_report_config.sql", "[Agent][ReportConfig]"); + patchSqlResource("/db/patch_agent_report_job_config.sql", "[Agent][ReportJobConfig]"); + patchAgentReportJobConfigColumns(); + patchSqlResource("/db/patch_agent_config.sql", "[Agent][Config]"); + patchAgentMenu(); + } + + private void patchAgentChatHistoryTables() { + patchSqlResource("/db/patch_agent_chat_history.sql", "[Agent][ChatHistory]"); + } + + private void patchAgentReportJobConfigColumns() { + addColumnIfMissing( + "agent_report_config", + "robotType", + "ALTER TABLE agent_report_config ADD COLUMN robotType VARCHAR(32) NOT NULL DEFAULT 'WE_COM' COMMENT '机器人类型' AFTER enabled"); + addColumnIfMissing( + "agent_report_job_config", + "robotType", + "ALTER TABLE agent_report_job_config ADD COLUMN robotType VARCHAR(32) NULL COMMENT '机器人类型覆盖值' AFTER groupName"); + addColumnIfMissing( + "agent_report_config", + "mailTo", + "ALTER TABLE agent_report_config ADD COLUMN mailTo VARCHAR(500) NULL COMMENT '邮件收件人' AFTER webhookUrl"); + addColumnIfMissing( + "agent_report_config", + "mailSubject", + "ALTER TABLE agent_report_config ADD COLUMN mailSubject VARCHAR(200) NULL COMMENT '邮件主题' AFTER mailTo"); + addColumnIfMissing( + "agent_report_job_config", + "mailTo", + "ALTER TABLE agent_report_job_config ADD COLUMN mailTo VARCHAR(500) NULL COMMENT '邮件收件人' AFTER webhookUrl"); + addColumnIfMissing( + "agent_report_job_config", + "mailSubject", + "ALTER TABLE agent_report_job_config ADD COLUMN mailSubject VARCHAR(200) NULL COMMENT '邮件主题' AFTER mailTo"); + addColumnIfMissing( + "agent_report_config", + "xxlJobGroupId", + "ALTER TABLE agent_report_config ADD COLUMN xxlJobGroupId INT NULL COMMENT 'XXL-JOB 执行器ID'"); + addColumnIfMissing( + "agent_report_job_config", + "xxlJobId", + "ALTER TABLE agent_report_job_config ADD COLUMN xxlJobId INT NULL COMMENT 'XXL-JOB 任务ID'"); + } + + private void addColumnIfMissing(String tableName, String columnName, String alterSql) { + try { + Integer count = jdbcTemplate.queryForObject(""" + SELECT COUNT(1) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = ? + AND column_name = ? + """, Integer.class, tableName, columnName); + if (count != null && count > 0) { + return; + } + jdbcTemplate.execute(alterSql); + log.info("[DB][Patch] added column {}.{}", tableName, columnName); + } catch (Exception ex) { + log.warn("[DB][Patch] add column {}.{} skipped: {}", tableName, columnName, ex.getMessage()); + } + } + + private void patchSqlResource(String resourcePath, String logPrefix) { + try (var inputStream = getClass().getResourceAsStream(resourcePath)) { + if (inputStream == null) { + log.warn("{} patch skipped: sql resource not found, path={}", logPrefix, resourcePath); + return; + } + String sql = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + for (String statement : sql.split(";")) { + if (!statement.isBlank()) { + jdbcTemplate.execute(statement); + } + } + log.info("{} patch applied", logPrefix); + } catch (Exception ex) { + log.warn("{} patch skipped: {}", logPrefix, ex.getMessage()); + } + } + + private void patchAgentMenu() { + try { + int menuRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-agent-page', 'sys:agent:list', 'AI 智能体', NULL, 'MENU', + '/agent', 'SystemAgent', '/system/agent/index', NULL, 'mdi:robot', 20, 'system.agent.title', + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), routePath = VALUES(routePath), routeName = VALUES(routeName), + component = VALUES(component), icon = VALUES(icon), sortOrder = VALUES(sortOrder), + title = VALUES(title) + """); + int codeGenRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-agent-code-gen', 'sys:agent:code:generate', 'AI 代码生成', 'menu-agent-page', 'PERM', + NULL, NULL, NULL, NULL, NULL, 601, NULL, + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), sortOrder = VALUES(sortOrder) + """); + int configRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-config-page', 'sys:config:list', '配置', NULL, 'MENU', + '/config', 'Config', NULL, NULL, 'mdi:cog-outline', 30, 'system.config.title', + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), routePath = VALUES(routePath), routeName = VALUES(routeName), + component = VALUES(component), icon = VALUES(icon), sortOrder = VALUES(sortOrder), + title = VALUES(title) + """); + int taskFunctionRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-task-function-config', 'sys:task-function:list', '任务功能配置', 'menu-config-page', 'MENU', + '/config/task-function', 'SystemTaskFunction', '/system/config/task-function', + NULL, 'mdi:function-variant', 601, 'system.config.taskFunctionTitle', + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), routePath = VALUES(routePath), routeName = VALUES(routeName), + component = VALUES(component), icon = VALUES(icon), sortOrder = VALUES(sortOrder), + title = VALUES(title) + """); + int taskFunctionSaveRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-task-function-save', 'sys:task-function:save', '任务功能保存', 'menu-task-function-config', 'PERM', + NULL, NULL, NULL, NULL, NULL, 602, NULL, + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), sortOrder = VALUES(sortOrder) + """); + int taskFunctionDeleteRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-task-function-delete', 'sys:task-function:delete', '任务功能删除', 'menu-task-function-config', 'PERM', + NULL, NULL, NULL, NULL, NULL, 603, NULL, + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), sortOrder = VALUES(sortOrder) + """); + int reportConfigRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-agent-report-config', 'sys:schedule-job:list', '计划任务配置', 'menu-config-page', 'MENU', + '/config/report-config', 'SystemAgentReportConfig', '/system/agent/report-config', + NULL, 'mdi:calendar-clock', 611, 'system.agent.tabReportConfig', + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), routePath = VALUES(routePath), routeName = VALUES(routeName), + component = VALUES(component), icon = VALUES(icon), sortOrder = VALUES(sortOrder), + title = VALUES(title) + """); + int reportConfigSaveRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-agent-report-config-save', 'sys:schedule-job:save', '计划任务保存', 'menu-agent-report-config', 'PERM', + NULL, NULL, NULL, NULL, NULL, 612, NULL, + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), sortOrder = VALUES(sortOrder) + """); + int reportConfigDeleteRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-agent-report-config-delete', 'sys:schedule-job:delete', '计划任务删除', 'menu-agent-report-config', 'PERM', + NULL, NULL, NULL, NULL, NULL, 613, NULL, + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), sortOrder = VALUES(sortOrder) + """); + int reportConfigExecuteRows = jdbcTemplate.update(""" + INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, + redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version + ) VALUES ( + 'menu-agent-report-config-execute', 'sys:schedule-job:execute', '计划任务执行', 'menu-agent-report-config', 'PERM', + NULL, NULL, NULL, NULL, NULL, 614, NULL, + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 + ) + ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), + menuType = VALUES(menuType), sortOrder = VALUES(sortOrder) + """); + int roleMenuRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-agent-', rm.roleId), rm.roleId, 'menu-agent-page' + FROM auth_role_menu rm + WHERE rm.menuId IN ( + 'menu-user-page', 'menu-role-page', 'menu-menu-page', + 'menu-dept-page', 'menu-data-perm-page' + ) + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-agent-page' + ) + """); + int codeGenRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-agent-code-gen-', rm.roleId), rm.roleId, 'menu-agent-code-gen' + FROM auth_role_menu rm + WHERE rm.menuId = 'menu-agent-page' + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-agent-code-gen' + ) + """); + int reportConfigRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-agent-report-config-', rm.roleId), rm.roleId, 'menu-agent-report-config' + FROM auth_role_menu rm + WHERE rm.menuId IN ( + 'menu-agent-page', 'menu-config-page', 'menu-user-page', 'menu-role-page', + 'menu-menu-page', 'menu-dept-page', 'menu-data-perm-page' + ) + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-agent-report-config' + ) + """); + int taskFunctionRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-task-function-config-', rm.roleId), rm.roleId, 'menu-task-function-config' + FROM auth_role_menu rm + WHERE rm.menuId IN ( + 'menu-agent-page', 'menu-config-page', 'menu-user-page', 'menu-role-page', + 'menu-menu-page', 'menu-dept-page', 'menu-data-perm-page' + ) + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-task-function-config' + ) + """); + int taskFunctionSaveRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-task-function-save-', rm.roleId), rm.roleId, 'menu-task-function-save' + FROM auth_role_menu rm + WHERE rm.menuId = 'menu-task-function-config' + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-task-function-save' + ) + """); + int taskFunctionDeleteRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-task-function-delete-', rm.roleId), rm.roleId, 'menu-task-function-delete' + FROM auth_role_menu rm + WHERE rm.menuId = 'menu-task-function-config' + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-task-function-delete' + ) + """); + int reportConfigSaveRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-agent-report-config-save-', rm.roleId), rm.roleId, 'menu-agent-report-config-save' + FROM auth_role_menu rm + WHERE rm.menuId = 'menu-agent-report-config' + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-agent-report-config-save' + ) + """); + int reportConfigDeleteRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-agent-report-config-delete-', rm.roleId), rm.roleId, 'menu-agent-report-config-delete' + FROM auth_role_menu rm + WHERE rm.menuId = 'menu-agent-report-config' + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-agent-report-config-delete' + ) + """); + int reportConfigExecuteRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-agent-report-config-execute-', rm.roleId), rm.roleId, 'menu-agent-report-config-execute' + FROM auth_role_menu rm + WHERE rm.menuId = 'menu-agent-report-config' + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-agent-report-config-execute' + ) + """); + int configRoleRows = jdbcTemplate.update(""" + INSERT INTO auth_role_menu (id, roleId, menuId) + SELECT CONCAT('rm-config-', rm.roleId), rm.roleId, 'menu-config-page' + FROM auth_role_menu rm + WHERE rm.menuId IN ( + 'menu-agent-page', 'menu-user-page', 'menu-role-page', 'menu-menu-page', + 'menu-dept-page', 'menu-data-perm-page' + ) + AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-config-page' + ) + """); + log.info("[RBAC] agent menu patch applied, menuRows={}, codeGenRows={}, configRows={}, taskFunctionRows={}, taskFunctionSaveRows={}, taskFunctionDeleteRows={}, reportConfigRows={}, reportConfigSaveRows={}, reportConfigDeleteRows={}, reportConfigExecuteRows={}, roleMenuRows={}, codeGenRoleRows={}, configRoleRows={}, taskFunctionRoleRows={}, taskFunctionSaveRoleRows={}, taskFunctionDeleteRoleRows={}, reportConfigRoleRows={}, reportConfigSaveRoleRows={}, reportConfigDeleteRoleRows={}, reportConfigExecuteRoleRows={}", + menuRows, codeGenRows, configRows, taskFunctionRows, taskFunctionSaveRows, taskFunctionDeleteRows, + reportConfigRows, reportConfigSaveRows, reportConfigDeleteRows, reportConfigExecuteRows, + roleMenuRows, codeGenRoleRows, configRoleRows, + taskFunctionRoleRows, taskFunctionSaveRoleRows, taskFunctionDeleteRoleRows, + reportConfigRoleRows, reportConfigSaveRoleRows, reportConfigDeleteRoleRows, reportConfigExecuteRoleRows); + } catch (Exception ex) { + log.warn("[RBAC] agent menu patch skipped: {}", ex.getMessage()); + } + } +} diff --git a/wcsApp/src/main/resources/application.yml b/wcsApp/src/main/resources/application.yml new file mode 100644 index 0000000..efb7fb7 --- /dev/null +++ b/wcsApp/src/main/resources/application.yml @@ -0,0 +1,121 @@ +spring: + rabbitmq: + host: 118.178.233.171 + port: 5672 + username: admin + password: Ittx@123 + virtual-host: / + connection-timeout: 10000 + cache: + channel: + size: 10 + data: + redis: + database: 1 + host: 118.178.233.171 + port: 6379 + password: Ittx@123 + timeout: 10s + lettuce: + pool: + max-active: 200 + max-wait: -1ms + max-idle: 10 + min-idle: 0 + connect-timeout: 1000 + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: jdbc:mysql://118.178.233.171:3306/will_system?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&zeroDateTimeBehavior=convertToNull + username: root + password: Ittx@123 + hikari: + auto-commit: true + maximum-pool-size: 10 + minimum-idle: 10 + connection-test-query: select 1 + connection-timeout: 20000 + + ai: + ollama: + base-url: http://localhost:11434 + chat: + options: + model: qwen2.5:7b + temperature: 0.2 + openai: + api-key: ${OPENAI_API_KEY:} + base-url: https://api.openai.com + chat: + options: + model: gpt-4o-mini + temperature: 0.2 + zhipuai: + api-key: ${ZHIPUAI_API_KEY:7980536e3ea04550b037db126899bb4f.QhE9ardDCIgT3SG3} + base-url: https://open.bigmodel.cn/api/coding/paas/ + chat: + options: + model: glm-4.7 + temperature: 0.2 + mcp: + client: + enabled: true + type: SYNC + toolcallback: + enabled: true + request-timeout: 60s + stdio: + servers-configuration: classpath:mcp-servers.json + +# 消息队列消费者开关:仅控制监听器/生产者,连接校验始终执行(与 Redis 一致) +will: + queue: + enabled: false + agent: + enabled: true + provider: zhipu + chat: + max-messages: 30 + mcp: + enabled: true + zentao: + enabled: true + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${ZENTAO_DB_URL:jdbc:mysql://rm-bp17678kj60u3vp59co.mysql.rds.aliyuncs.com:3306/zentao?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&zeroDateTimeBehavior=convertToNull} + username: ${ZENTAO_DB_USERNAME:zentao_read} + password: ${ZENTAO_DB_PASSWORD:yakljpHPY8NN} + maximum-pool-size: 5 + minimum-idle: 1 + connection-timeout: 10000 + report: + xxl-job: + enabled: ${XXL_JOB_ENABLED:true} + admin-addresses: ${XXL_JOB_ADMIN_ADDRESSES:http://118.178.233.171:8081} + admin-username: ${XXL_JOB_ADMIN_USERNAME:willsystem} + admin-password: ${XXL_JOB_ADMIN_PASSWORD:Tt123456} + access-token: ${XXL_JOB_ACCESS_TOKEN:x41TdRp1NDa75g} + job-group-id: ${XXL_JOB_GROUP_ID:3} + appname: ${XXL_JOB_APPNAME:will-system-agent} + address: ${XXL_JOB_EXECUTOR_ADDRESS:} + ip: ${XXL_JOB_EXECUTOR_IP:} + port: ${XXL_JOB_EXECUTOR_PORT:9999} + log-path: ${XXL_JOB_LOG_PATH:logs/xxl-job/jobhandler} + log-retention-days: ${XXL_JOB_LOG_RETENTION_DAYS:30} + +sa-token: + token-name: satoken + timeout: 7200 + active-timeout: -1 + is-concurrent: false + is-share: false + token-style: simple-uuid + is-log: true + jwt-secret-key: will20250101 + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + typeAliasesPackage: com.will.coreData.entity + mapperLocations: classpath:mapper/**/*.xml diff --git a/wcsApp/src/main/resources/db/alter_auth_menu.sql b/wcsApp/src/main/resources/db/alter_auth_menu.sql new file mode 100644 index 0000000..71e5950 --- /dev/null +++ b/wcsApp/src/main/resources/db/alter_auth_menu.sql @@ -0,0 +1,17 @@ +-- auth_menu 路由字段扩展(已有库执行一次,列已存在时会报错可忽略) +USE will_system; + +ALTER TABLE auth_menu + ADD COLUMN menuType VARCHAR(16) NOT NULL DEFAULT 'PERM' COMMENT 'DIR|MENU|PERM' AFTER parentId; + +ALTER TABLE auth_menu + ADD COLUMN routePath VARCHAR(128) NULL AFTER menuType, + ADD COLUMN routeName VARCHAR(64) NULL AFTER routePath, + ADD COLUMN component VARCHAR(256) NULL AFTER routeName, + ADD COLUMN redirect VARCHAR(128) NULL AFTER component, + ADD COLUMN icon VARCHAR(64) NULL AFTER redirect, + ADD COLUMN sortOrder INT NOT NULL DEFAULT 0 AFTER icon, + ADD COLUMN title VARCHAR(128) NULL AFTER sortOrder; + +ALTER TABLE auth_data_permission + ADD COLUMN roleId VARCHAR(64) NULL AFTER permissionId; diff --git a/wcsApp/src/main/resources/db/init_rbac.sql b/wcsApp/src/main/resources/db/init_rbac.sql new file mode 100644 index 0000000..dfacc05 --- /dev/null +++ b/wcsApp/src/main/resources/db/init_rbac.sql @@ -0,0 +1,215 @@ +-- Will System RBAC 初始化脚本 + +-- 使用前请先执行 alter_auth_menu.sql(已有库) + +-- 默认管理员:admin / 123456 + + + +SET NAMES utf8mb4; + + + +-- ========== 角色 ========== + +INSERT INTO auth_role (id, code, name, departmentId, created, createdBy, lastUpdated, lastUpdatedBy, version) + +VALUES ('role-admin', 'admin', '系统管理员', NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE code = VALUES(code), name = VALUES(name); + + + +INSERT INTO auth_role (id, code, name, departmentId, created, createdBy, lastUpdated, lastUpdatedBy, version) + +VALUES ('role-user', 'user', '普通用户', NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE code = VALUES(code), name = VALUES(name); + + + +-- ========== 菜单:路由(DIR/MENU) + 权限点(PERM),code 即 API 权限码 ========== + +INSERT INTO auth_menu ( + + id, code, name, parentId, menuType, routePath, routeName, component, redirect, icon, sortOrder, title, + + created, createdBy, lastUpdated, lastUpdatedBy, version + +) VALUES + +-- 工作台(登录即可见) + +('route-dashboard', 'route:dashboard', '工作台', NULL, 'DIR', '/dashboard', 'Dashboard', NULL, '/dashboard/workspace', 'lucide:layout-dashboard', -1, 'page.dashboard.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('route-dashboard-analytics', 'route:dashboard:analytics', '分析页', 'route-dashboard', 'MENU', 'analytics', 'Analytics', '/dashboard/analytics/index', NULL, NULL, 1, 'page.dashboard.analytics', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('route-dashboard-workspace', 'route:dashboard:workspace', '工作区', 'route-dashboard', 'MENU', 'workspace', 'Workspace', '/dashboard/workspace/index', NULL, NULL, 2, 'page.dashboard.workspace', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +-- 系统管理 + +('route-system', 'route:system', '系统管理', NULL, 'DIR', '/system', 'System', NULL, NULL, 'ion:settings-outline', 10, 'system.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-dept-page', 'sys:department:list', '部门管理', 'route-system', 'MENU', 'department', 'SystemDepartment', '/system/department/list', NULL, 'charm:organisation', 11, 'system.dept.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-user-page', 'sys:user:list', '用户管理', 'route-system', 'MENU', 'user', 'SystemUser', '/system/user/index', NULL, 'mdi:user', 12, 'system.user.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-role-page', 'sys:role:list', '角色管理', 'route-system', 'MENU', 'role', 'SystemRole', '/system/role/index', NULL, 'mdi:account-group', 13, 'system.role.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-menu-page', 'sys:menu:list', '菜单管理', 'route-system', 'MENU', 'menu', 'SystemMenu', '/system/menu/index', NULL, 'mdi:menu', 14, 'system.menu.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +-- 部门 API 权限点 + +('menu-dept-table', 'sys:department:table:list', '部门分页', 'menu-dept-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 101, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-dept-save', 'sys:department:save', '部门保存', 'menu-dept-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 102, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-dept-delete', 'sys:department:delete', '部门删除', 'menu-dept-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 103, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-dept-users', 'sys:department:users', '部门用户', 'menu-dept-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 104, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-dept-test', 'sys:department:test', '部门测试', 'menu-dept-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 105, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +-- 用户 API 权限点 + +('menu-user-save', 'sys:user:save', '用户保存', 'menu-user-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 201, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-user-delete', 'sys:user:delete', '用户删除', 'menu-user-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 202, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +-- 角色 API 权限点 + +('menu-role-save', 'sys:role:save', '角色保存', 'menu-role-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 301, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-role-delete', 'sys:role:delete', '角色删除', 'menu-role-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 302, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-role-assign', 'sys:role:assign', '角色授权', 'menu-role-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 303, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +-- 菜单 API 权限点 + +('menu-menu-save', 'sys:menu:save', '菜单保存', 'menu-menu-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 401, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-menu-delete', 'sys:menu:delete', '菜单删除', 'menu-menu-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 402, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +-- 数据权限 + +('menu-data-perm-page', 'sys:data-permission:list', '数据权限', 'route-system', 'MENU', 'data-permission', 'SystemDataPermission', '/system/data-permission/index', NULL, 'mdi:database-lock', 15, 'system.dataPermission.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-data-perm-save', 'sys:data-permission:save', '数据权限保存', 'menu-data-perm-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 501, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-agent-page', 'sys:agent:list', 'AI 智能体', NULL, 'MENU', '/agent', 'SystemAgent', '/system/agent/index', NULL, 'mdi:robot', 20, 'system.agent.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('menu-agent-code-gen', 'sys:agent:code:generate', 'AI 代码生成', 'menu-agent-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 601, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE + + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), menuType = VALUES(menuType), + + routePath = VALUES(routePath), routeName = VALUES(routeName), component = VALUES(component), + + redirect = VALUES(redirect), icon = VALUES(icon), sortOrder = VALUES(sortOrder), title = VALUES(title); + + + +-- ========== 权限点扩展表(admin 角色自动合并 ApiPermissions.ALL_API_PERMISSIONS) ========== + +INSERT INTO auth_permission_info (id, code, name, created, createdBy, lastUpdated, lastUpdatedBy, version) VALUES + +('perm-dept-list', 'sys:department:list', '部门列表', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('perm-user-list', 'sys:user:list', '用户列表', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('perm-role-list', 'sys:role:list', '角色列表', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), + +('perm-menu-list', 'sys:menu:list', '菜单列表', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE code = VALUES(code), name = VALUES(name); + + + +-- ========== admin 角色绑定全部 MENU/PERM 权限点(不含 route: 目录) ========== + +INSERT INTO auth_role_menu (id, roleId, menuId) VALUES + +('rm-001', 'role-admin', 'menu-dept-page'), + +('rm-002', 'role-admin', 'menu-dept-table'), + +('rm-003', 'role-admin', 'menu-dept-save'), + +('rm-004', 'role-admin', 'menu-dept-delete'), + +('rm-005', 'role-admin', 'menu-dept-users'), + +('rm-006', 'role-admin', 'menu-dept-test'), + +('rm-007', 'role-admin', 'menu-user-page'), + +('rm-008', 'role-admin', 'menu-user-save'), + +('rm-009', 'role-admin', 'menu-user-delete'), + +('rm-010', 'role-admin', 'menu-role-page'), + +('rm-011', 'role-admin', 'menu-role-save'), + +('rm-012', 'role-admin', 'menu-role-delete'), + +('rm-013', 'role-admin', 'menu-role-assign'), + +('rm-014', 'role-admin', 'menu-menu-page'), + +('rm-015', 'role-admin', 'menu-menu-save'), + +('rm-016', 'role-admin', 'menu-menu-delete'), + +('rm-017', 'role-admin', 'menu-data-perm-page'), + +('rm-018', 'role-admin', 'menu-data-perm-save'), + +('rm-019', 'role-admin', 'menu-agent-page'), + +('rm-020', 'role-admin', 'menu-agent-code-gen') + +ON DUPLICATE KEY UPDATE roleId = VALUES(roleId), menuId = VALUES(menuId); + + + +-- ========== 默认管理员 admin / 123456 ========== + +INSERT INTO auth_user (id, account, userName, email, phoneNumber, status, lastLoginTime, created, createdBy, lastUpdated, lastUpdatedBy, version) + +VALUES ('user-admin', 'admin', '系统管理员', 'admin@will.local', NULL, 1, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE account = VALUES(account), userName = VALUES(userName), status = 1; + + + +INSERT INTO auth_user_password (id, userId, password, created, createdBy, lastUpdated, lastUpdatedBy, version) + +VALUES ('pwd-admin', 'user-admin', '123456', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE password = VALUES(password); + + + +INSERT INTO auth_user_role (id, userId, roleId, departmentId, created, createdBy, lastUpdated, lastUpdatedBy, version) + +VALUES ('ur-admin', 'user-admin', 'role-admin', NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE roleId = VALUES(roleId); + + + +-- ========== 数据权限示例(admin 角色:全部数据) ========== + +INSERT INTO auth_data_permission (id, permissionId, roleId, userId, readPermission, editPermission, deletePermission, created, createdBy, lastUpdated, lastUpdatedBy, version) + +VALUES ('dp-admin-all', 'perm-dept-list', 'role-admin', NULL, 'ALL', 'ALL', 'ALL', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) + +ON DUPLICATE KEY UPDATE readPermission = VALUES(readPermission), editPermission = VALUES(editPermission), deletePermission = VALUES(deletePermission); + +-- ========== 普通用户角色:本部门数据 ========== +INSERT INTO auth_data_permission (id, permissionId, roleId, userId, readPermission, editPermission, deletePermission, created, createdBy, lastUpdated, lastUpdatedBy, version) +VALUES ('dp-user-dept', 'perm-dept-list', 'role-user', NULL, 'DEPT', 'DEPT', 'DEPT', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) +ON DUPLICATE KEY UPDATE readPermission = VALUES(readPermission), editPermission = VALUES(editPermission), deletePermission = VALUES(deletePermission); + diff --git a/wcsApp/src/main/resources/db/patch_agent_chat_history.sql b/wcsApp/src/main/resources/db/patch_agent_chat_history.sql new file mode 100644 index 0000000..cbc5f45 --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_agent_chat_history.sql @@ -0,0 +1,36 @@ +CREATE TABLE IF NOT EXISTS agent_chat_session ( + id VARCHAR(64) NOT NULL COMMENT '会话ID', + userId VARCHAR(64) NOT NULL COMMENT '所属用户ID', + title VARCHAR(120) NOT NULL COMMENT '会话标题', + scope VARCHAR(20) NOT NULL DEFAULT 'zentao' COMMENT '范围:zentao/system', + backendSessionId VARCHAR(128) NULL COMMENT 'Spring AI ChatMemory 会话ID', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + created DATETIME NULL COMMENT '创建时间', + createdBy VARCHAR(64) NULL COMMENT '创建人', + lastUpdated DATETIME NULL COMMENT '修改时间', + lastUpdatedBy VARCHAR(64) NULL COMMENT '修改人', + version INT NULL DEFAULT 0 COMMENT '版本号', + PRIMARY KEY (id), + KEY idx_agent_chat_session_user_updated (userId, deleted, lastUpdated), + KEY idx_agent_chat_session_backend (backendSessionId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 智能体会话'; + +CREATE TABLE IF NOT EXISTS agent_chat_message ( + id VARCHAR(64) NOT NULL COMMENT '消息ID', + sessionId VARCHAR(64) NOT NULL COMMENT '会话ID', + userId VARCHAR(64) NOT NULL COMMENT '所属用户ID', + role VARCHAR(20) NOT NULL COMMENT '角色:user/assistant', + content LONGTEXT NULL COMMENT '消息内容', + sortOrder INT NOT NULL DEFAULT 0 COMMENT '消息顺序', + created DATETIME NULL COMMENT '创建时间', + createdBy VARCHAR(64) NULL COMMENT '创建人', + lastUpdated DATETIME NULL COMMENT '修改时间', + lastUpdatedBy VARCHAR(64) NULL COMMENT '修改人', + version INT NULL DEFAULT 0 COMMENT '版本号', + PRIMARY KEY (id), + KEY idx_agent_chat_message_session_sort (sessionId, sortOrder), + KEY idx_agent_chat_message_user_created (userId, created), + CONSTRAINT fk_agent_chat_message_session + FOREIGN KEY (sessionId) REFERENCES agent_chat_session (id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 智能体会话消息'; diff --git a/wcsApp/src/main/resources/db/patch_agent_config.sql b/wcsApp/src/main/resources/db/patch_agent_config.sql new file mode 100644 index 0000000..8b169f3 --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_agent_config.sql @@ -0,0 +1,61 @@ +CREATE TABLE IF NOT EXISTS agent_config_group ( + id VARCHAR(64) NOT NULL COMMENT '配置分类ID', + code VARCHAR(64) NOT NULL COMMENT '配置分类编码', + name VARCHAR(120) NOT NULL COMMENT '配置分类名称', + description VARCHAR(500) NULL COMMENT '配置分类说明', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + sortOrder INT NOT NULL DEFAULT 100 COMMENT '排序', + created DATETIME NULL COMMENT '创建时间', + createdBy VARCHAR(64) NULL COMMENT '创建人', + lastUpdated DATETIME NULL COMMENT '修改时间', + lastUpdatedBy VARCHAR(64) NULL COMMENT '修改人', + version INT NULL DEFAULT 0 COMMENT '版本号', + PRIMARY KEY (id), + UNIQUE KEY uk_agent_config_group_code (code), + KEY idx_agent_config_group_enabled_sort (enabled, sortOrder) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 统一配置分类'; + +CREATE TABLE IF NOT EXISTS agent_config_item ( + id VARCHAR(64) NOT NULL COMMENT '配置项ID', + groupId VARCHAR(64) NOT NULL COMMENT '配置分类ID', + code VARCHAR(64) NOT NULL COMMENT '配置项编码', + name VARCHAR(120) NOT NULL COMMENT '配置项名称', + description VARCHAR(500) NULL COMMENT '配置项说明', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + sortOrder INT NOT NULL DEFAULT 100 COMMENT '排序', + created DATETIME NULL COMMENT '创建时间', + createdBy VARCHAR(64) NULL COMMENT '创建人', + lastUpdated DATETIME NULL COMMENT '修改时间', + lastUpdatedBy VARCHAR(64) NULL COMMENT '修改人', + version INT NULL DEFAULT 0 COMMENT '版本号', + PRIMARY KEY (id), + UNIQUE KEY uk_agent_config_item_group_code (groupId, code), + KEY idx_agent_config_item_group_enabled_sort (groupId, enabled, sortOrder) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 统一配置项'; + +INSERT INTO agent_config_group ( + id, code, name, description, enabled, sortOrder, + created, createdBy, lastUpdated, lastUpdatedBy, version +) VALUES ( + 'config-group-task-function', 'TASK_FUNCTION', '任务功能配置', + '计划任务可选择的任务功能列表', + 1, 100, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 +) ON DUPLICATE KEY UPDATE + name = VALUES(name), + description = VALUES(description), + enabled = VALUES(enabled), + sortOrder = VALUES(sortOrder); + +INSERT INTO agent_config_item ( + id, groupId, code, name, description, enabled, sortOrder, + created, createdBy, lastUpdated, lastUpdatedBy, version +) VALUES ( + 'config-item-zentao-daily-effort', 'config-group-task-function', + 'ZENTAO_DAILY_EFFORT', '禅道工时报表推送', + '按执行范围和统计偏移天数生成禅道工时报表,并推送到企业微信机器人', + 1, 100, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 +) ON DUPLICATE KEY UPDATE + name = VALUES(name), + description = VALUES(description), + enabled = VALUES(enabled), + sortOrder = VALUES(sortOrder); diff --git a/wcsApp/src/main/resources/db/patch_agent_menu.sql b/wcsApp/src/main/resources/db/patch_agent_menu.sql new file mode 100644 index 0000000..ac87faf --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_agent_menu.sql @@ -0,0 +1,18 @@ +-- AI 代码生成 Agent 菜单(已有库执行一次) +USE will_system; + +INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version +) VALUES +('menu-agent-page', 'sys:agent:list', 'AI 智能体', NULL, 'MENU', '/agent', 'SystemAgent', '/system/agent/index', NULL, 'mdi:robot', 20, 'system.agent.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), +('menu-agent-code-gen', 'sys:agent:code:generate', 'AI 代码生成', 'menu-agent-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 601, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) +ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), menuType = VALUES(menuType), + routePath = VALUES(routePath), routeName = VALUES(routeName), component = VALUES(component), + icon = VALUES(icon), sortOrder = VALUES(sortOrder), title = VALUES(title); + +INSERT INTO auth_role_menu (id, roleId, menuId) VALUES +('rm-agent', 'role-admin', 'menu-agent-page'), +('rm-agent-code-gen', 'role-admin', 'menu-agent-code-gen') +ON DUPLICATE KEY UPDATE roleId = VALUES(roleId), menuId = VALUES(menuId); diff --git a/wcsApp/src/main/resources/db/patch_agent_report_config.sql b/wcsApp/src/main/resources/db/patch_agent_report_config.sql new file mode 100644 index 0000000..fbfe045 --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_agent_report_config.sql @@ -0,0 +1,36 @@ +CREATE TABLE IF NOT EXISTS agent_report_config ( + id VARCHAR(64) NOT NULL COMMENT '配置ID', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '总开关', + robotEnabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '机器人开关', + robotType VARCHAR(32) NOT NULL DEFAULT 'WE_COM' COMMENT '机器人类型', + webhookUrl VARCHAR(500) NULL COMMENT '机器人 Webhook', + mailTo VARCHAR(500) NULL COMMENT '邮件收件人', + mailSubject VARCHAR(200) NULL COMMENT '邮件主题', + dailyEffortEnabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '每日工时报表开关', + localSchedulerEnabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '本地定时开关', + cron VARCHAR(64) NOT NULL DEFAULT '0 0 10 * * ?' COMMENT '每日执行 Cron', + zone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT '时区', + daysAgo INT NOT NULL DEFAULT 2 COMMENT '统计几天前', + groupName VARCHAR(100) NOT NULL DEFAULT 'TSO' COMMENT '禅道用户组', + xxlJobEnabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'XXL-JOB 开关', + xxlAdminAddresses VARCHAR(500) NULL COMMENT 'XXL-JOB Admin 地址', + xxlJobGroupId INT NULL COMMENT 'XXL-JOB executor group id', + xxlAppname VARCHAR(100) NOT NULL DEFAULT 'will-system-agent' COMMENT 'XXL-JOB 执行器名称', + xxlExecutorPort INT NOT NULL DEFAULT 9999 COMMENT 'XXL-JOB 执行器端口', + created DATETIME NULL COMMENT '创建时间', + createdBy VARCHAR(64) NULL COMMENT '创建人', + lastUpdated DATETIME NULL COMMENT '修改时间', + lastUpdatedBy VARCHAR(64) NULL COMMENT '修改人', + version INT NULL DEFAULT 0 COMMENT '版本号', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 报表推送配置'; + +INSERT INTO agent_report_config ( + id, enabled, robotType, dailyEffortEnabled, localSchedulerEnabled, + cron, zone, daysAgo, groupName, xxlJobEnabled, xxlAppname, xxlExecutorPort, + created, createdBy, lastUpdated, lastUpdatedBy, version +) VALUES ( + 'zentao-daily-effort-wecom', 1, 'WE_COM', 1, 1, + '0 0 10 * * ?', 'Asia/Shanghai', 2, 'TSO', 0, 'will-system-agent', 9999, + NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0 +) ON DUPLICATE KEY UPDATE id = id; diff --git a/wcsApp/src/main/resources/db/patch_agent_report_job_config.sql b/wcsApp/src/main/resources/db/patch_agent_report_job_config.sql new file mode 100644 index 0000000..1d1e621 --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_agent_report_job_config.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS agent_report_job_config ( + id VARCHAR(64) NOT NULL COMMENT '任务配置ID', + name VARCHAR(120) NOT NULL COMMENT '任务名称', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + jobType VARCHAR(64) NOT NULL DEFAULT 'ZENTAO_DAILY_EFFORT' COMMENT '任务类型', + cron VARCHAR(64) NOT NULL DEFAULT '0 0 10 * * ?' COMMENT 'Cron', + zone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT '时区', + daysAgo INT NOT NULL DEFAULT 2 COMMENT '统计几天前', + groupName VARCHAR(100) NOT NULL DEFAULT 'TSO' COMMENT '禅道用户组', + robotType VARCHAR(32) NULL COMMENT '机器人类型覆盖值', + webhookUrl VARCHAR(500) NULL COMMENT '机器人 Webhook 覆盖值', + mailTo VARCHAR(500) NULL COMMENT '邮件收件人', + mailSubject VARCHAR(200) NULL COMMENT '邮件主题', + xxlJobId INT NULL COMMENT 'XXL-JOB 任务ID', + localSchedulerEnabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '本地定时开关', + remark VARCHAR(500) NULL COMMENT '备注', + created DATETIME NULL COMMENT '创建时间', + createdBy VARCHAR(64) NULL COMMENT '创建人', + lastUpdated DATETIME NULL COMMENT '修改时间', + lastUpdatedBy VARCHAR(64) NULL COMMENT '修改人', + version INT NULL DEFAULT 0 COMMENT '版本号', + PRIMARY KEY (id), + KEY idx_agent_report_job_enabled_type (enabled, jobType) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 报表计划任务配置'; diff --git a/wcsApp/src/main/resources/db/patch_agent_top_menu.sql b/wcsApp/src/main/resources/db/patch_agent_top_menu.sql new file mode 100644 index 0000000..7325b50 --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_agent_top_menu.sql @@ -0,0 +1,32 @@ +-- 已有库增量:将 AI 智能体调整为一级主菜单 +USE will_system; + +UPDATE auth_menu +SET name = 'AI 智能体', + code = 'sys:agent:list', + parentId = NULL, + routePath = '/agent', + routeName = 'SystemAgent', + component = '/system/agent/index', + icon = 'mdi:robot', + sortOrder = 20, + title = 'system.agent.title' +WHERE id = 'menu-agent-page'; + +INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version +) VALUES +('menu-agent-code-gen', 'sys:agent:code:generate', 'AI 代码生成', 'menu-agent-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 601, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) +ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), menuType = VALUES(menuType), + sortOrder = VALUES(sortOrder); + +INSERT INTO auth_role_menu (id, roleId, menuId) +SELECT CONCAT('rm-agent-code-gen-', rm.roleId), rm.roleId, 'menu-agent-code-gen' +FROM auth_role_menu rm +WHERE rm.menuId = 'menu-agent-page' +AND NOT EXISTS ( + SELECT 1 FROM auth_role_menu x + WHERE x.roleId = rm.roleId AND x.menuId = 'menu-agent-code-gen' +); diff --git a/wcsApp/src/main/resources/db/patch_data_permission_menu.sql b/wcsApp/src/main/resources/db/patch_data_permission_menu.sql new file mode 100644 index 0000000..6d1184c --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_data_permission_menu.sql @@ -0,0 +1,21 @@ +-- 已有库增量:数据权限管理菜单(执行一次) +USE will_system; + +INSERT INTO auth_menu ( + id, code, name, parentId, menuType, routePath, routeName, component, redirect, icon, sortOrder, title, + created, createdBy, lastUpdated, lastUpdatedBy, version +) VALUES +('menu-data-perm-page', 'sys:data-permission:list', '数据权限', 'route-system', 'MENU', 'data-permission', 'SystemDataPermission', '/system/data-permission/index', NULL, 'mdi:database-lock', 15, 'system.dataPermission.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0), +('menu-data-perm-save', 'sys:data-permission:save', '数据权限保存', 'menu-data-perm-page', 'PERM', NULL, NULL, NULL, NULL, NULL, 501, NULL, NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0) +ON DUPLICATE KEY UPDATE + code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), menuType = VALUES(menuType), + routePath = VALUES(routePath), routeName = VALUES(routeName), component = VALUES(component), + icon = VALUES(icon), sortOrder = VALUES(sortOrder), title = VALUES(title); + +INSERT INTO auth_role_menu (id, roleId, menuId) VALUES +('rm-017', 'role-admin', 'menu-data-perm-page'), +('rm-018', 'role-admin', 'menu-data-perm-save') +ON DUPLICATE KEY UPDATE roleId = VALUES(roleId), menuId = VALUES(menuId); + +UPDATE auth_menu SET parentId = 'menu-data-perm-page' +WHERE code = 'sys:data-permission:save' AND menuType = 'PERM'; diff --git a/wcsApp/src/main/resources/db/patch_menu_permission_parent.sql b/wcsApp/src/main/resources/db/patch_menu_permission_parent.sql new file mode 100644 index 0000000..50a7ee1 --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_menu_permission_parent.sql @@ -0,0 +1,20 @@ +-- 已有库增量:修复按钮/API 权限点的父级菜单 +USE will_system; + +UPDATE auth_menu SET parentId = 'menu-dept-page' +WHERE menuType = 'PERM' AND code LIKE 'sys:department:%'; + +UPDATE auth_menu SET parentId = 'menu-user-page' +WHERE menuType = 'PERM' AND code LIKE 'sys:user:%'; + +UPDATE auth_menu SET parentId = 'menu-role-page' +WHERE menuType = 'PERM' AND code LIKE 'sys:role:%'; + +UPDATE auth_menu SET parentId = 'menu-menu-page' +WHERE menuType = 'PERM' AND code LIKE 'sys:menu:%'; + +UPDATE auth_menu SET parentId = 'menu-data-perm-page' +WHERE menuType = 'PERM' AND code LIKE 'sys:data-permission:%'; + +UPDATE auth_menu SET parentId = 'menu-agent-page' +WHERE menuType = 'PERM' AND code LIKE 'sys:agent:%'; diff --git a/wcsApp/src/main/resources/db/zentao_report_indexes.sql b/wcsApp/src/main/resources/db/zentao_report_indexes.sql new file mode 100644 index 0000000..5796157 --- /dev/null +++ b/wcsApp/src/main/resources/db/zentao_report_indexes.sql @@ -0,0 +1,41 @@ +-- 禅道报表查询性能索引建议。 +-- 执行前请 DBA 在生产库先用 SHOW INDEX / EXPLAIN 校验,避免和已有索引重复。 +-- MySQL 8 可用 ALGORITHM=INPLACE, LOCK=NONE 视表结构与版本而定。 + +-- 工时统计:按 objectType + date 范围过滤,按 account/task 分组,JOIN task。 +ALTER TABLE zt_effort + ADD INDEX idx_effort_object_date_account_task (objectType, `date`, account, objectID); + +-- 用户过滤:账号 JOIN、姓名精确匹配;LIKE '%姓名%' 不能完全利用普通索引,但 realname IN 可用。 +ALTER TABLE zt_user + ADD INDEX idx_user_deleted_account (deleted, account), + ADD INDEX idx_user_deleted_realname (deleted, realname); + +-- 用户组过滤:按 group name 找 group,再用 group/account 回表。 +ALTER TABLE zt_group + ADD INDEX idx_group_name (name); + +ALTER TABLE zt_usergroup + ADD INDEX idx_usergroup_group_account (`group`, account), + ADD INDEX idx_usergroup_account_group (account, `group`); + +-- 任务完成统计:按完成时间过滤,并按完成人/状态聚合。 +ALTER TABLE zt_task + ADD INDEX idx_task_deleted_finished_date_user_status (deleted, finishedDate, finishedBy, assignedTo, status), + ADD INDEX idx_task_deleted_assigned_date_user_status (deleted, assignedDate, assignedTo, status), + ADD INDEX idx_task_deleted_closed_date_user_status (deleted, closedDate, assignedTo, status), + ADD INDEX idx_task_deleted_opened_date_user_status (deleted, openedDate, assignedTo, status), + ADD INDEX idx_task_project_deleted_status (project, deleted, status); + +-- 需求完成统计:按指派/关闭/创建时间过滤,并按指派人/状态/阶段聚合。 +ALTER TABLE zt_story + ADD INDEX idx_story_deleted_assigned_date_user_status_stage (deleted, assignedDate, assignedTo, status, stage), + ADD INDEX idx_story_deleted_closed_date_user_status_stage (deleted, closedDate, assignedTo, status, stage), + ADD INDEX idx_story_deleted_opened_date_user_status_stage (deleted, openedDate, assignedTo, status, stage); + +-- 产品/项目名称查询。目前 LIKE '%关键字%' 对普通索引利用有限;精确/前缀匹配时可用。 +ALTER TABLE zt_product + ADD INDEX idx_product_deleted_name (deleted, name); + +ALTER TABLE zt_project + ADD INDEX idx_project_deleted_name (deleted, name); diff --git a/wcsApp/src/main/resources/mcp-servers.json b/wcsApp/src/main/resources/mcp-servers.json new file mode 100644 index 0000000..da39e4f --- /dev/null +++ b/wcsApp/src/main/resources/mcp-servers.json @@ -0,0 +1,3 @@ +{ + "mcpServers": {} +} diff --git a/wcsService/build.gradle b/wcsService/build.gradle new file mode 100644 index 0000000..03e4748 --- /dev/null +++ b/wcsService/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +group = 'com.will' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/will_server b/will_server new file mode 160000 index 0000000..18f391d --- /dev/null +++ b/will_server @@ -0,0 +1 @@ +Subproject commit 18f391d7432fe98ece7da414128ae65d1661e375