From 9d100f9465c269fc6eedf82558de8d47f9a81850 Mon Sep 17 00:00:00 2001 From: will Date: Wed, 5 Aug 2026 16:38:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E6=B7=BB=E5=8A=A0AI=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BB=A3=E7=90=86=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成Spring AI框架支持大模型对话 - 实现聊天历史记录管理功能 - 添加代码生成功能支持 - 集成MCP协议工具调用 - 实现流式响应和会话管理 - 添加系统提示词和上下文管理 - 支持多租户会话隔离 - 配置自动化装配和依赖注入 - 实现错误处理和异常捕获 - 添加用户权限验证和审计追踪 - 支持对话历史持久化存储 - 实现智能时间上下文识别 --- .gitignore | 42 + .../8.13/dependencies-accessors/gc.properties | 0 .../caches/8.13/file-changes/last-build.bin | Bin 0 -> 1 bytes .../caches/8.13/fileContent/fileContent.lock | Bin 0 -> 17 bytes .../caches/8.13/fileHashes/fileHashes.bin | Bin 0 -> 19497 bytes .../caches/8.13/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .gradle-codex/caches/8.13/gc.properties | 0 .../classes/settings/_BuildScript_.class | Bin 0 -> 2623 bytes .../instrumented/settings/_BuildScript_.class | Bin 0 -> 3283 bytes .../metadata.bin | 2 + .../metadata/metadata.bin | Bin 0 -> 2 bytes .../cp_proj/_BuildScript_$_run_closure1.class | Bin 0 -> 2721 bytes .../classes/cp_proj/_BuildScript_.class | Bin 0 -> 2107 bytes .../cp_proj/_BuildScript_$_run_closure1.class | Bin 0 -> 4216 bytes .../instrumented/cp_proj/_BuildScript_.class | Bin 0 -> 2576 bytes .../metadata.bin | Bin 0 -> 160 bytes .../metadata/metadata.bin | Bin 0 -> 1 bytes ...BuildScript_$_run_closure1$_closure2.class | Bin 0 -> 2436 bytes ...BuildScript_$_run_closure1$_closure3.class | Bin 0 -> 2646 bytes .../_BuildScript_$_run_closure1.class | Bin 0 -> 2570 bytes .../classes/cp_settings/_BuildScript_.class | Bin 0 -> 2021 bytes ...BuildScript_$_run_closure1$_closure2.class | Bin 0 -> 3931 bytes ...BuildScript_$_run_closure1$_closure3.class | Bin 0 -> 4141 bytes .../_BuildScript_$_run_closure1.class | Bin 0 -> 4065 bytes .../cp_settings/_BuildScript_.class | Bin 0 -> 2490 bytes .../metadata.bin | 1 + .../metadata/metadata.bin | Bin 0 -> 1 bytes .../caches/8.13/groovy-dsl/gc.properties | 0 .../caches/8.13/md-rule/md-rule.lock | Bin 0 -> 17 bytes .../caches/8.13/md-supplier/md-supplier.lock | Bin 0 -> 17 bytes .../caches/8.13/transforms/gc.properties | 0 .gradle-codex/caches/CACHEDIR.TAG | 4 + .../cp_proj.lock.lock | Bin 0 -> 2 bytes .../cp_proj.receipt | 0 ...nfiqgqt95ogf691jwspmy9$_run_closure1.class | Bin 0 -> 4718 bytes .../build_b5jnfiqgqt95ogf691jwspmy9.class | Bin 0 -> 3064 bytes .../settings.lock.lock | Bin 0 -> 2 bytes .../settings.receipt | 0 .../settings_7jyhbgwrblrfd17akpfy4iwtz.class | Bin 0 -> 3762 bytes .../cp_settings.lock.lock | Bin 0 -> 2 bytes .../cp_settings.receipt | 0 ...17akpfy4iwtz$_run_closure1$_closure2.class | Bin 0 -> 4439 bytes ...17akpfy4iwtz$_run_closure1$_closure3.class | Bin 0 -> 4649 bytes ...hbgwrblrfd17akpfy4iwtz$_run_closure1.class | Bin 0 -> 4621 bytes .../settings_7jyhbgwrblrfd17akpfy4iwtz.class | Bin 0 -> 2990 bytes .gradle-codex/caches/jars-9/gc.properties | 0 .gradle-codex/caches/jars-9/jars-9.lock | Bin 0 -> 17 bytes .../caches/journal-1/file-access.bin | Bin 0 -> 18647 bytes .../caches/journal-1/file-access.properties | 2 + .gradle-codex/caches/journal-1/journal-1.lock | Bin 0 -> 17 bytes .gradle-codex/caches/modules-2/gc.properties | 0 .../metadata-2.107/module-metadata.bin | Bin 0 -> 18497 bytes .../metadata-2.107/resource-at-url.bin | Bin 0 -> 18497 bytes .gradle-codex/caches/modules-2/modules-2.lock | Bin 0 -> 17 bytes .../daemon/8.13/daemon-24024.out.log | 446 ++++++++ .gradle-codex/daemon/8.13/registry.bin | Bin 0 -> 9 bytes .gradle-codex/daemon/8.13/registry.bin.lock | Bin 0 -> 17 bytes .gradle-codex/daemon/CACHEDIR.TAG | 4 + .../x86_64-windows-gnu/gradle-fileevents.dll | Bin 0 -> 465408 bytes .../gradle-fileevents.dll.lock | 1 + .../windows-amd64/native-platform.dll | Bin 0 -> 141312 bytes .../windows-amd64/native-platform.dll.lock | 1 + .../native/jansi/1.18/windows64/jansi.dll | Bin 0 -> 26112 bytes .../8.13/release-features.rendered | 0 .idea/.gitignore | 8 + .idea/gradle.xml | 30 + .idea/inspectionProfiles/Project_Default.xml | 8 + .idea/misc.xml | 10 + .idea/uiDesigner.xml | 124 ++ .idea/vcs.xml | 7 + application.properties | 2 + build.gradle | 65 ++ businessAgent/build.gradle | 19 + .../codegen/BusinessCodeGenAgentService.java | 90 ++ .../codegen/BusinessCodeGenGraph.java | 165 +++ .../businessAgent/codegen/CodeArtifact.java | 12 + .../businessAgent/codegen/CodeGenRequest.java | 13 + .../businessAgent/codegen/CodeGenResult.java | 17 + .../businessAgent/codegen/CodeGenState.java | 88 ++ .../BusinessAgentAutoConfiguration.java | 27 + .../BusinessAgentToolConfiguration.java | 35 + .../controller/AgentCodeController.java | 34 + .../controller/AgentZentaoController.java | 39 + .../tool/WillSystemAgentTools.java | 29 + .../tool/WillSystemOrgTools.java | 212 ++++ .../businessAgent/tool/ZentaoReportTools.java | 1003 +++++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + businessJob/build.gradle | 21 + .../businessJob/mail/MailMessageClient.java | 51 + .../businessJob/robot/RobotMessageClient.java | 102 ++ .../ZentaoEffortReportQueryService.java | 144 +++ .../zentao/ZentaoWeComReportService.java | 163 +++ coreAgent/_mcp_test.gradle | 3 + coreAgent/build.gradle | 19 + .../agent/context/AgentOperatorContext.java | 32 + .../coreAgent/agent/dto/AgentChatRequest.java | 13 + .../coreAgent/agent/dto/AgentChatResult.java | 12 + .../coreAgent/agent/dto/AgentDefinition.java | 15 + .../coreAgent/agent/dto/AgentMetaResult.java | 19 + .../coreAgent/agent/dto/McpStatusResult.java | 14 + .../agent/prompt/WillSystemPrompts.java | 199 ++++ .../agent/service/AgentChatService.java | 171 +++ .../agent/service/AgentMcpStatusService.java | 23 + .../agent/service/AgentMetaService.java | 68 ++ .../config/AgentAutoConfiguration.java | 25 + .../config/AgentChatClientConfiguration.java | 46 + .../config/AgentMemoryConfiguration.java | 23 + .../config/AiChatModelConfiguration.java | 66 ++ .../config/ZentaoDataSourceConfiguration.java | 29 + .../framework/mcp/McpContextService.java | 14 + .../framework/mcp/McpContextServiceImpl.java | 133 +++ .../UserContextAwareToolCallbackProvider.java | 72 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + coreApi/build.gradle | 17 + .../controller/AgentChatController.java | 194 ++++ .../AgentChatHistoryController.java | 71 ++ .../controller/AgentMcpController.java | 37 + .../controller/AgentReportController.java | 76 ++ .../AgentTaskFunctionController.java | 47 + .../coreApi/controller/AuthController.java | 38 + .../AuthDataPermissionController.java | 40 + .../controller/AuthDepartmentController.java | 183 +++ .../controller/AuthMenuController.java | 54 + .../AuthPermissionInfoController.java | 23 + .../controller/AuthRoleController.java | 68 ++ .../controller/AuthUserController.java | 69 ++ .../coreApi/controller/MenuController.java | 27 + .../controller/TableCommonController.java | 24 + coreAuth/build.gradle | 15 + .../auth/annotation/RequirePermission.java | 13 + .../com/will/auth/annotation/RequireRole.java | 11 + .../auth/aspect/PermissionCheckAspect.java | 40 + .../auth/config/AuthAutoConfiguration.java | 31 + .../will/auth/config/StpInterfaceImpl.java | 25 + .../will/auth/config/SysSessionParameter.java | 43 + .../will/auth/config/SysSessionSetting.java | 12 + .../auth/config/UserContextSaTokenBridge.java | 23 + .../exception/GlobalExceptionHandler.java | 34 + .../auth/handler/SaTokenNotLoginHandler.java | 5 + .../ApiPrefixGuardInterceptor.java | 37 + .../interceptor/UserContextInterceptor.java | 33 + .../com/will/auth/service/AuthService.java | 82 ++ coreCommon/build.gradle | 3 + .../com/will/coreCommon/BaseController.java | 4 + .../java/com/will/coreCommon/BaseListDTO.java | 10 + .../java/com/will/coreCommon/BasePage.java | 18 + .../java/com/will/coreCommon/BaseService.java | 5 + .../will/coreCommon/auth/ApiPermissions.java | 88 ++ .../com/will/coreCommon/auth/DataScope.java | 19 + .../com/will/coreCommon/auth/MenuTypes.java | 17 + .../coreCommon/context/DataAuthContext.java | 25 + .../will/coreCommon/context/UserContext.java | 56 + .../coreCommon/general/ResponseMessage.java | 48 + .../general/ResponseMessageFactory.java | 28 + .../coreCommon/general/SystemConstants.java | 13 + .../java/com/will/coreCommon/util/Coder.java | 82 ++ coreData/build.gradle | 17 + .../com/will/coreData/BaseDaoServiceImpl.java | 25 + .../java/com/will/coreData/BaseEntity.java | 44 + .../java/com/will/coreData/BaseMapper.java | 4 + .../will/coreData/EntityMapperRegistry.java | 48 + .../com/will/coreData/IBaseDaoService.java | 11 + .../will/coreData/MyMetaObjectHandler.java | 38 + .../com/will/coreData/MybatisPlusConfig.java | 25 + .../com/will/coreData/MybatisPlusUtils.java | 67 ++ .../MysqlConnectionStartupValidator.java | 40 + .../will/coreData/auth/AuthLoginSupport.java | 63 ++ .../will/coreData/auth/AuthRbacSupport.java | 97 ++ .../dao/IAgentChatMessageService.java | 7 + .../dao/IAgentChatSessionService.java | 7 + .../dao/IAgentConfigGroupService.java | 7 + .../coreData/dao/IAgentConfigItemService.java | 7 + .../dao/IAgentReportConfigService.java | 7 + .../dao/IAgentReportJobConfigService.java | 7 + .../dao/IAuthDataPermissionService.java | 18 + .../coreData/dao/IAuthDepartmentService.java | 14 + .../dao/IAuthDepartmentUserService.java | 16 + .../will/coreData/dao/IAuthMenuService.java | 16 + .../dao/IAuthPermissionInfoService.java | 16 + .../coreData/dao/IAuthRoleMenuService.java | 16 + .../will/coreData/dao/IAuthRoleService.java | 16 + .../coreData/dao/IAuthRoleSettingService.java | 16 + .../dao/IAuthUserPasswordService.java | 17 + .../coreData/dao/IAuthUserRoleService.java | 16 + .../coreData/dao/IAuthUserSaltService.java | 17 + .../will/coreData/dao/IAuthUserService.java | 18 + .../dao/impl/AgentChatMessageServiceImpl.java | 12 + .../dao/impl/AgentChatSessionServiceImpl.java | 12 + .../dao/impl/AgentConfigGroupServiceImpl.java | 12 + .../dao/impl/AgentConfigItemServiceImpl.java | 12 + .../impl/AgentReportConfigServiceImpl.java | 12 + .../impl/AgentReportJobConfigServiceImpl.java | 12 + .../impl/AuthDataPermissionServiceImpl.java | 22 + .../dao/impl/AuthDepartmentServiceImpl.java | 21 + .../impl/AuthDepartmentUserServiceImpl.java | 13 + .../dao/impl/AuthMenuServiceImpl.java | 20 + .../impl/AuthPermissionInfoServiceImpl.java | 20 + .../dao/impl/AuthRoleMenuServiceImpl.java | 28 + .../dao/impl/AuthRoleServiceImpl.java | 23 + .../dao/impl/AuthRoleSettingServiceImpl.java | 20 + .../dao/impl/AuthUserPasswordServiceImpl.java | 25 + .../dao/impl/AuthUserRoleServiceImpl.java | 20 + .../dao/impl/AuthUserSaltServiceImpl.java | 25 + .../dao/impl/AuthUserServiceImpl.java | 17 + .../coreData/entity/AgentChatMessage.java | 37 + .../coreData/entity/AgentChatSession.java | 37 + .../coreData/entity/AgentConfigGroup.java | 37 + .../will/coreData/entity/AgentConfigItem.java | 40 + .../coreData/entity/AgentReportConfig.java | 73 ++ .../coreData/entity/AgentReportJobConfig.java | 64 ++ .../coreData/entity/AuthDataPermission.java | 47 + .../will/coreData/entity/AuthDepartment.java | 52 + .../coreData/entity/AuthDepartmentUser.java | 36 + .../com/will/coreData/entity/AuthMenu.java | 78 ++ .../coreData/entity/AuthPermissionInfo.java | 37 + .../com/will/coreData/entity/AuthRole.java | 42 + .../will/coreData/entity/AuthRoleMenu.java | 40 + .../will/coreData/entity/AuthRoleSetting.java | 81 ++ .../com/will/coreData/entity/AuthUser.java | 55 + .../coreData/entity/AuthUserPassword.java | 39 + .../will/coreData/entity/AuthUserRole.java | 50 + .../will/coreData/entity/AuthUserSalt.java | 39 + .../mapper/AgentChatMessageMapper.java | 9 + .../mapper/AgentChatSessionMapper.java | 9 + .../mapper/AgentConfigGroupMapper.java | 9 + .../mapper/AgentConfigItemMapper.java | 9 + .../mapper/AgentReportConfigMapper.java | 9 + .../mapper/AgentReportJobConfigMapper.java | 9 + .../mapper/AuthDataPermissionMapper.java | 22 + .../coreData/mapper/AuthDepartmentMapper.java | 18 + .../mapper/AuthDepartmentUserMapper.java | 18 + .../will/coreData/mapper/AuthMenuMapper.java | 18 + .../mapper/AuthPermissionInfoMapper.java | 18 + .../will/coreData/mapper/AuthRoleMapper.java | 18 + .../coreData/mapper/AuthRoleMenuMapper.java | 18 + .../mapper/AuthRoleSettingMapper.java | 18 + .../will/coreData/mapper/AuthUserMapper.java | 18 + .../mapper/AuthUserPasswordMapper.java | 18 + .../coreData/mapper/AuthUserRoleMapper.java | 18 + .../coreData/mapper/AuthUserSaltMapper.java | 18 + coreJob/build.gradle | 12 + .../framework/config/XxlJobConfiguration.java | 69 ++ .../ZentaoReportIntegrationProperties.java | 27 + .../framework/xxl/XxlJobAdminClient.java | 251 +++++ .../XxlJobReportJobScheduleSyncService.java | 35 + .../xxl/ZentaoEffortReportJobHandler.java | 62 + coreQueue/build.gradle | 11 + .../RabbitMqConnectionStartupValidator.java | 41 + .../config/ConditionalOnQueueEnabled.java | 19 + .../will/coreQueue/core/QueueConsumer.java | 14 + .../core/QueueConsumerBeanRegistrar.java | 71 ++ .../coreQueue/core/QueueConsumerProvider.java | 35 + .../coreQueue/core/QueueConsumerService.java | 7 + .../coreQueue/core/QueueListenerFactory.java | 8 + .../com/will/coreQueue/core/QueueMessage.java | 13 + .../core/QueueMessageListenerHandle.java | 17 + .../will/coreQueue/core/QueueProducer.java | 7 + .../rabbit/MyRabbitConfigProperties.java | 35 + .../rabbit/QueueModuleConfiguration.java | 12 + .../rabbit/RabbitAutoConfiguration.java | 72 ++ .../rabbit/RabbitListenerFactory.java | 88 ++ .../will/coreQueue/rabbit/RabbitProducer.java | 23 + coreRedis/build.gradle | 8 + .../RedisConnectionStartupValidator.java | 45 + .../coreRedis/config/RedisAdvancedConfig.java | 95 ++ .../coreRedis/config/RedisTemplateConfig.java | 29 + .../will/coreRedis/service/CacheService.java | 110 ++ .../service/RedisDistributedLock.java | 96 ++ .../coreRedis/service/StringRedisService.java | 40 + coreService/build.gradle | 13 + .../com/will/coreService/IBaseService.java | 18 + .../coreService/dto/AgentChatMessageDTO.java | 15 + .../dto/AgentChatMessageSaveDTO.java | 11 + .../coreService/dto/AgentChatSessionDTO.java | 18 + .../dto/AgentChatSessionSaveDTO.java | 12 + .../coreService/dto/AgentReportConfigDTO.java | 24 + .../dto/AgentReportJobConfigDTO.java | 21 + .../dto/AgentReportJobTypeDTO.java | 13 + .../coreService/dto/AgentTaskFunctionDTO.java | 14 + .../com/will/coreService/dto/BaseService.java | 39 + .../coreService/dto/DataPermissionDTO.java | 14 + .../dto/DataPermissionListItemDTO.java | 14 + .../dto/DataPermissionSaveDTO.java | 12 + .../will/coreService/dto/DepartmentDTO.java | 20 + .../com/will/coreService/dto/LLMModelDTO.java | 24 + .../com/will/coreService/dto/MenuDTO.java | 44 + .../com/will/coreService/dto/RoleDTO.java | 14 + .../com/will/coreService/dto/UserDTO.java | 20 + .../com/will/coreService/dto/UserSaveDTO.java | 19 + .../com/will/coreService/enums/RobotType.java | 32 + .../service/AgentChatHistoryService.java | 173 +++ .../service/AgentReportConfigService.java | 88 ++ .../service/AgentReportJobConfigService.java | 150 +++ .../AgentReportJobScheduleSyncService.java | 12 + .../service/AgentTaskFunctionService.java | 112 ++ .../coreService/service/DataAuthService.java | 286 +++++ .../service/DepartmentService.java | 528 +++++++++ .../service/MenuManageService.java | 479 ++++++++ .../will/coreService/service/RoleService.java | 136 +++ .../will/coreService/service/UserService.java | 450 ++++++++ settings.gradle | 25 + wcsApp/build.gradle | 21 + .../com/will/wcsApp/WcsAppApplication.java | 17 + .../PrimaryDataSourceConfiguration.java | 30 + .../wcsApp/startup/RbacMenuPatchRunner.java | 384 +++++++ wcsApp/src/main/resources/application.yml | 121 ++ .../src/main/resources/db/alter_auth_menu.sql | 17 + wcsApp/src/main/resources/db/init_rbac.sql | 215 ++++ .../resources/db/patch_agent_chat_history.sql | 36 + .../main/resources/db/patch_agent_config.sql | 61 + .../main/resources/db/patch_agent_menu.sql | 18 + .../db/patch_agent_report_config.sql | 36 + .../db/patch_agent_report_job_config.sql | 24 + .../resources/db/patch_agent_top_menu.sql | 32 + .../db/patch_data_permission_menu.sql | 21 + .../db/patch_menu_permission_parent.sql | 20 + .../resources/db/zentao_report_indexes.sql | 41 + wcsApp/src/main/resources/mcp-servers.json | 3 + wcsService/build.gradle | 19 + will_server | 1 + 320 files changed, 13327 insertions(+) create mode 100644 .gitignore create mode 100644 .gradle-codex/caches/8.13/dependencies-accessors/gc.properties create mode 100644 .gradle-codex/caches/8.13/file-changes/last-build.bin create mode 100644 .gradle-codex/caches/8.13/fileContent/fileContent.lock create mode 100644 .gradle-codex/caches/8.13/fileHashes/fileHashes.bin create mode 100644 .gradle-codex/caches/8.13/fileHashes/fileHashes.lock create mode 100644 .gradle-codex/caches/8.13/gc.properties create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/classes/settings/_BuildScript_.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/instrumented/settings/_BuildScript_.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata.bin create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/246b309f3c26a98efecb348aca8fbd72/metadata/metadata.bin create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_$_run_closure1.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/classes/cp_proj/_BuildScript_.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_$_run_closure1.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/instrumented/cp_proj/_BuildScript_.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata.bin create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/4cb5408c196f5e807160dfc01d07c73a/metadata/metadata.bin create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure2.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1$_closure3.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_$_run_closure1.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/classes/cp_settings/_BuildScript_.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure2.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1$_closure3.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_$_run_closure1.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/instrumented/cp_settings/_BuildScript_.class create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata.bin create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/bb95da4301dd6245a4881793e83a9b33/metadata/metadata.bin create mode 100644 .gradle-codex/caches/8.13/groovy-dsl/gc.properties create mode 100644 .gradle-codex/caches/8.13/md-rule/md-rule.lock create mode 100644 .gradle-codex/caches/8.13/md-supplier/md-supplier.lock create mode 100644 .gradle-codex/caches/8.13/transforms/gc.properties create mode 100644 .gradle-codex/caches/CACHEDIR.TAG create mode 100644 .gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj.lock.lock create mode 100644 .gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj.receipt create mode 100644 .gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9$_run_closure1.class create mode 100644 .gradle-codex/caches/jars-9/6d777ffb002336699c7f4657ad3ab818/cp_proj/build_b5jnfiqgqt95ogf691jwspmy9.class create mode 100644 .gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings.lock.lock create mode 100644 .gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings.receipt create mode 100644 .gradle-codex/caches/jars-9/976f35808aa0c5187a9b3c6a796b993c/settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class create mode 100644 .gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings.lock.lock create mode 100644 .gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings.receipt create mode 100644 .gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure2.class create mode 100644 .gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1$_closure3.class create mode 100644 .gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz$_run_closure1.class create mode 100644 .gradle-codex/caches/jars-9/e4d094ab536cb1c905034a6cd42eb595/cp_settings/settings_7jyhbgwrblrfd17akpfy4iwtz.class create mode 100644 .gradle-codex/caches/jars-9/gc.properties create mode 100644 .gradle-codex/caches/jars-9/jars-9.lock create mode 100644 .gradle-codex/caches/journal-1/file-access.bin create mode 100644 .gradle-codex/caches/journal-1/file-access.properties create mode 100644 .gradle-codex/caches/journal-1/journal-1.lock create mode 100644 .gradle-codex/caches/modules-2/gc.properties create mode 100644 .gradle-codex/caches/modules-2/metadata-2.107/module-metadata.bin create mode 100644 .gradle-codex/caches/modules-2/metadata-2.107/resource-at-url.bin create mode 100644 .gradle-codex/caches/modules-2/modules-2.lock create mode 100644 .gradle-codex/daemon/8.13/daemon-24024.out.log create mode 100644 .gradle-codex/daemon/8.13/registry.bin create mode 100644 .gradle-codex/daemon/8.13/registry.bin.lock create mode 100644 .gradle-codex/daemon/CACHEDIR.TAG create mode 100644 .gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll create mode 100644 .gradle-codex/native/0.2.7/x86_64-windows-gnu/gradle-fileevents.dll.lock create mode 100644 .gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll create mode 100644 .gradle-codex/native/1def1411415f61bf3af743bc5b6707747c0891f09f0c88961ee8f79bc544acac/windows-amd64/native-platform.dll.lock create mode 100644 .gradle-codex/native/jansi/1.18/windows64/jansi.dll create mode 100644 .gradle-codex/notifications/8.13/release-features.rendered create mode 100644 .idea/.gitignore create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 application.properties create mode 100644 build.gradle create mode 100644 businessAgent/build.gradle create mode 100644 businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenAgentService.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/codegen/BusinessCodeGenGraph.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/codegen/CodeArtifact.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenRequest.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenResult.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/codegen/CodeGenState.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentAutoConfiguration.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/config/BusinessAgentToolConfiguration.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/controller/AgentCodeController.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/controller/AgentZentaoController.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemAgentTools.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/tool/WillSystemOrgTools.java create mode 100644 businessAgent/src/main/java/com/will/businessAgent/tool/ZentaoReportTools.java create mode 100644 businessAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 businessJob/build.gradle create mode 100644 businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java create mode 100644 businessJob/src/main/java/com/will/businessJob/robot/RobotMessageClient.java create mode 100644 businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java create mode 100644 businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeComReportService.java create mode 100644 coreAgent/_mcp_test.gradle create mode 100644 coreAgent/build.gradle create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/context/AgentOperatorContext.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatRequest.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentChatResult.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentDefinition.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/dto/AgentMetaResult.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/dto/McpStatusResult.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/prompt/WillSystemPrompts.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentChatService.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMcpStatusService.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/agent/service/AgentMetaService.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentAutoConfiguration.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentChatClientConfiguration.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/config/AgentMemoryConfiguration.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/config/AiChatModelConfiguration.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/config/ZentaoDataSourceConfiguration.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextService.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/mcp/McpContextServiceImpl.java create mode 100644 coreAgent/src/main/java/com/will/coreAgent/framework/tool/UserContextAwareToolCallbackProvider.java create mode 100644 coreAgent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 coreApi/build.gradle create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AgentChatController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AgentChatHistoryController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AgentMcpController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AgentReportController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AgentTaskFunctionController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AuthController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AuthDataPermissionController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AuthDepartmentController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AuthMenuController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AuthPermissionInfoController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AuthRoleController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/AuthUserController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/MenuController.java create mode 100644 coreApi/src/main/java/com/will/coreApi/controller/TableCommonController.java create mode 100644 coreAuth/build.gradle create mode 100644 coreAuth/src/main/java/com/will/auth/annotation/RequirePermission.java create mode 100644 coreAuth/src/main/java/com/will/auth/annotation/RequireRole.java create mode 100644 coreAuth/src/main/java/com/will/auth/aspect/PermissionCheckAspect.java create mode 100644 coreAuth/src/main/java/com/will/auth/config/AuthAutoConfiguration.java create mode 100644 coreAuth/src/main/java/com/will/auth/config/StpInterfaceImpl.java create mode 100644 coreAuth/src/main/java/com/will/auth/config/SysSessionParameter.java create mode 100644 coreAuth/src/main/java/com/will/auth/config/SysSessionSetting.java create mode 100644 coreAuth/src/main/java/com/will/auth/config/UserContextSaTokenBridge.java create mode 100644 coreAuth/src/main/java/com/will/auth/exception/GlobalExceptionHandler.java create mode 100644 coreAuth/src/main/java/com/will/auth/handler/SaTokenNotLoginHandler.java create mode 100644 coreAuth/src/main/java/com/will/auth/interceptor/ApiPrefixGuardInterceptor.java create mode 100644 coreAuth/src/main/java/com/will/auth/interceptor/UserContextInterceptor.java create mode 100644 coreAuth/src/main/java/com/will/auth/service/AuthService.java create mode 100644 coreCommon/build.gradle create mode 100644 coreCommon/src/main/java/com/will/coreCommon/BaseController.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/BaseListDTO.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/BasePage.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/BaseService.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/auth/ApiPermissions.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/auth/DataScope.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/auth/MenuTypes.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/context/DataAuthContext.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/context/UserContext.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessage.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/general/ResponseMessageFactory.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/general/SystemConstants.java create mode 100644 coreCommon/src/main/java/com/will/coreCommon/util/Coder.java create mode 100644 coreData/build.gradle create mode 100644 coreData/src/main/java/com/will/coreData/BaseDaoServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/BaseEntity.java create mode 100644 coreData/src/main/java/com/will/coreData/BaseMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/EntityMapperRegistry.java create mode 100644 coreData/src/main/java/com/will/coreData/IBaseDaoService.java create mode 100644 coreData/src/main/java/com/will/coreData/MyMetaObjectHandler.java create mode 100644 coreData/src/main/java/com/will/coreData/MybatisPlusConfig.java create mode 100644 coreData/src/main/java/com/will/coreData/MybatisPlusUtils.java create mode 100644 coreData/src/main/java/com/will/coreData/MysqlConnectionStartupValidator.java create mode 100644 coreData/src/main/java/com/will/coreData/auth/AuthLoginSupport.java create mode 100644 coreData/src/main/java/com/will/coreData/auth/AuthRbacSupport.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAgentChatMessageService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAgentChatSessionService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAgentConfigGroupService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAgentConfigItemService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAgentReportConfigService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAgentReportJobConfigService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthDataPermissionService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthDepartmentUserService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthMenuService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthPermissionInfoService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthRoleMenuService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthRoleService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthRoleSettingService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthUserPasswordService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthUserRoleService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthUserSaltService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/IAuthUserService.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AgentChatMessageServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AgentChatSessionServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigGroupServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AgentConfigItemServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AgentReportConfigServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AgentReportJobConfigServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthDataPermissionServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthDepartmentUserServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthMenuServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthPermissionInfoServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleMenuServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthRoleSettingServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthUserPasswordServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthUserRoleServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthUserSaltServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/dao/impl/AuthUserServiceImpl.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AgentChatMessage.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AgentChatSession.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AgentConfigGroup.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AgentConfigItem.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AgentReportConfig.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AgentReportJobConfig.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthDataPermission.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthDepartment.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthDepartmentUser.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthMenu.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthPermissionInfo.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthRole.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthRoleMenu.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthRoleSetting.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthUser.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthUserPassword.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthUserRole.java create mode 100644 coreData/src/main/java/com/will/coreData/entity/AuthUserSalt.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AgentChatMessageMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AgentChatSessionMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AgentConfigGroupMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AgentConfigItemMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AgentReportConfigMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AgentReportJobConfigMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthDataPermissionMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthDepartmentUserMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthMenuMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthPermissionInfoMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthRoleMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthRoleMenuMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthRoleSettingMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthUserMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthUserPasswordMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthUserRoleMapper.java create mode 100644 coreData/src/main/java/com/will/coreData/mapper/AuthUserSaltMapper.java create mode 100644 coreJob/build.gradle create mode 100644 coreJob/src/main/java/com/will/coreJob/framework/config/XxlJobConfiguration.java create mode 100644 coreJob/src/main/java/com/will/coreJob/framework/config/ZentaoReportIntegrationProperties.java create mode 100644 coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobAdminClient.java create mode 100644 coreJob/src/main/java/com/will/coreJob/framework/xxl/XxlJobReportJobScheduleSyncService.java create mode 100644 coreJob/src/main/java/com/will/coreJob/framework/xxl/ZentaoEffortReportJobHandler.java create mode 100644 coreQueue/build.gradle create mode 100644 coreQueue/src/main/java/com/will/coreQueue/RabbitMqConnectionStartupValidator.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/config/ConditionalOnQueueEnabled.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumer.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerBeanRegistrar.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerProvider.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueConsumerService.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueListenerFactory.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueMessage.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueMessageListenerHandle.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/core/QueueProducer.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/rabbit/MyRabbitConfigProperties.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/rabbit/QueueModuleConfiguration.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitAutoConfiguration.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitListenerFactory.java create mode 100644 coreQueue/src/main/java/com/will/coreQueue/rabbit/RabbitProducer.java create mode 100644 coreRedis/build.gradle create mode 100644 coreRedis/src/main/java/com/will/coreRedis/RedisConnectionStartupValidator.java create mode 100644 coreRedis/src/main/java/com/will/coreRedis/config/RedisAdvancedConfig.java create mode 100644 coreRedis/src/main/java/com/will/coreRedis/config/RedisTemplateConfig.java create mode 100644 coreRedis/src/main/java/com/will/coreRedis/service/CacheService.java create mode 100644 coreRedis/src/main/java/com/will/coreRedis/service/RedisDistributedLock.java create mode 100644 coreRedis/src/main/java/com/will/coreRedis/service/StringRedisService.java create mode 100644 coreService/build.gradle create mode 100644 coreService/src/main/java/com/will/coreService/IBaseService.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentChatMessageDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentChatMessageSaveDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentChatSessionDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentChatSessionSaveDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentReportConfigDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentReportJobConfigDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentReportJobTypeDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/AgentTaskFunctionDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/BaseService.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/DataPermissionDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/DataPermissionListItemDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/DataPermissionSaveDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/DepartmentDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/LLMModelDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/MenuDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/RoleDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/UserDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/dto/UserSaveDTO.java create mode 100644 coreService/src/main/java/com/will/coreService/enums/RobotType.java create mode 100644 coreService/src/main/java/com/will/coreService/service/AgentChatHistoryService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/AgentReportConfigService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/AgentReportJobConfigService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/AgentReportJobScheduleSyncService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/AgentTaskFunctionService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/DataAuthService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/DepartmentService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/MenuManageService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/RoleService.java create mode 100644 coreService/src/main/java/com/will/coreService/service/UserService.java create mode 100644 settings.gradle create mode 100644 wcsApp/build.gradle create mode 100644 wcsApp/src/main/java/com/will/wcsApp/WcsAppApplication.java create mode 100644 wcsApp/src/main/java/com/will/wcsApp/config/PrimaryDataSourceConfiguration.java create mode 100644 wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java create mode 100644 wcsApp/src/main/resources/application.yml create mode 100644 wcsApp/src/main/resources/db/alter_auth_menu.sql create mode 100644 wcsApp/src/main/resources/db/init_rbac.sql create mode 100644 wcsApp/src/main/resources/db/patch_agent_chat_history.sql create mode 100644 wcsApp/src/main/resources/db/patch_agent_config.sql create mode 100644 wcsApp/src/main/resources/db/patch_agent_menu.sql create mode 100644 wcsApp/src/main/resources/db/patch_agent_report_config.sql create mode 100644 wcsApp/src/main/resources/db/patch_agent_report_job_config.sql create mode 100644 wcsApp/src/main/resources/db/patch_agent_top_menu.sql create mode 100644 wcsApp/src/main/resources/db/patch_data_permission_menu.sql create mode 100644 wcsApp/src/main/resources/db/patch_menu_permission_parent.sql create mode 100644 wcsApp/src/main/resources/db/zentao_report_indexes.sql create mode 100644 wcsApp/src/main/resources/mcp-servers.json create mode 100644 wcsService/build.gradle create mode 160000 will_server 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 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8fb69eb4a81558248af24f7c31add4289bf59a2b GIT binary patch literal 17 TcmZP$op#@<{$AJ%1}FdkID`bF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6907523df724e7cd2147cf88fb9db483be27da09 GIT binary patch literal 19497 zcmeI3|5HqB9LG<^wXMu<z zc>2(v3p>Q}{H-B<-NsiuBjdxbv%Dxezt=u#03EMYv;1I6&u<5hwiVFvK`g)gHnHUX z#oQE;A5VhklelPCLOPP$#VJOV3o0~ z(}&hw#_|bXo|OqHhZ<<^@-cs7`+a$_-A;0C63c^~cSId2+!ROa#CZ8qyNglq3_4&M*K2FaQHE00S@p126ysFaQHE00S@p126ys zFaQHE00S@p126ysFaQHE00S@p126ysFaQHE00S@p126ysFaQHzyMc-P0!H)4FyWB9 zIG?LrJh7_ubk?-HzBf`k_xY@dC9d=x$DbvO*?<+-b{!&bjJhdBqpxBEALbkMXZfUv~Ss1K4>W=ZVMIo z8d?0wp+?fUpQa9a5v@rsQTEdXRfK#_NEe6+Rn=ChA}PVV&QG#vL|Uqq5_SAYzDb4s z5)L&6Uk{Y%OQxutPbp*k_MTExqV@y8^?4*FE~#^7se--p`>(tbF{D}2NC-XMgTPtE zgv-$SbZhdy{j2KJd>b>1JqTgoYYELwn4QC{Og490W{;$`Fe1BJ>6OFplMvn#L(m6+ z+*V{2W4j>Ajk6?5OOBR}5ZrZ|1hSyF(t*8fIFfR9qv#)OI>o`JXQ1sk=GGT+oja j*@Tb?&&6RKGa4q;8T3=}Aof1|;kZ1lLqd@@i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..15c7a0d3ae3bfea14eb8111fcdce58fc308bf72a GIT binary patch literal 17 UcmZSXy2-wOalPdw1_%%X05*FBZ~y=R literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..51891e08371e41bae491793974af8fb9c27e78c9 GIT binary patch literal 2623 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4360ab703906094301a807a336d28e7e7d887e7e GIT binary patch literal 2107 zcmb7FU31%15Ixs+qC`;=$N7L#3t*I+V?*Vla0YE%u^ zYHfJ&kawz9iE|&gd{^c*YR}EG)k7<>6GoJ z!%(OQ7{h7s1KsZsp5q)IX)bSC1YtV1c4oW6Al2)HuvubY_ZWt6n6~MC!jM$+y9`sg z7WYcJWmQd&uez>&#IU61_XY>>ZO?qbHG}LsWUrk%t|%CTA|ruu1*4ExSiYe3vguc$Hy6?T_Doliw9Jy(S}z*P{mZF@F&; z66P7k1YafJb-C>^?5LHfUqRXHLGPmC`-Au2#H)_n6ugDp7~aI&4AY{a)nH&Yxu*w% zz;KDEjz>NFM9oN_Qyt<-$TN(HxeyaakUf10E{W$N-jz|n6^6xu!x&K#4)WTO#|3Gt z4ZTeRFF_-9h(SC{grPW~8y97*U=d3)uHtV$@_po9hk@fRY>@dK{>53)YQ#* z_^`ZLo7fo>xHc%#Q zu-LeSaV3-$tf3_2zd_sM#i1o^{!fe{{{w3jV%(Ooh1&`~!>VZg4r%>-i2I0?aEDe7 zHATTLW@OyOHwtcHT_jv%n2k3+QdFq#;Y&B7Joo}lyd!1{29de4=u+M z>470racC%Zb=TDQEq+Hl#KtsjzU4pI=WcW}PLei$+SJ?8yHd7o?glh*lBisDeAnQ& zOp9Zl%1Y4^O{Qg`4Iuq9B#h803&QBjFfCZx()2XwO{FtIr;IKpDuv&X{*~Ss8uV1? z1ldj@6WmQ>Ch}S*OyQLcyo#CGr9!HU%O}Xz3WV{-A9yE)hh&fdr>6)x)B~CgLDMNL z1SyDA*2r3bE);mEto@irX%3)GCj$Ts+370-m8sPnv>>=a`mo zorEw zi_A+Ap85#YzcEG6V|@D;Icz*bri*(UC%9W9*Y6*rTKElmPl7YgF(J_`B+w#U!G4y` LeWKxj&IH(h$(t}x literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2658aaf063d777db982d49317addda9b989d0f54 GIT binary patch literal 4216 zcmb_f`Bxj)75*N`mJq{Y*(M>0o1`T%2#gF#le9Q?0Xw#8mKF#Nc3X8UJ&Cc!j5-?O z($!s(G~M@oZ_|Bo>;!P!wEd|$r+-sU+waZ{LNY^7+~Xhg=FOXX@4L%)7yawsul^0d zqj*i>-rPjZDi`MSu2u1JeL1&g=kjHzT66VC97pEjxp`%I3INB6id6U9{{(-5B=BgQJH+$it&U<49+F=M&qb&;EeO1r& zto)>0n6-4d?i05*YFg>ZD&M9>E<;`+=-tzRg>zZo{hZ+=d zbulyV6umiJ)ULMkn(TXzS+?aJS2*0?KpCpM^JHLAVP9WKdy~!AjP?(lkJdW$yr@;B zTA3|I`r|I#okR`VOZljY}IQkuG?i`1<%L__L82yysO!=w(6!|*K;F+hw}|6 zpZ5f;tOtUELIN(~X;v!bwFH- z_(mHI`KE!71Q&i=;k3B#*5>h|Kkz@`}_Do8@`7ha+rzIJRdP#=|UVoW|t0< zcgyuGC0ByhxZ6x8mA7fK2yl7FjFGth6hBMiC-^y=QdN5=by=5~l0ICjr;0@x%T{%H zt+A~d_!UwD!xt4!ym=o4eXU*zwuXn_C=8f|Le^e&@_w+-IL;-Gi{UtW(vn#zCqmbf*;yP9#rgnu6Kf`NQ+E zJ8I5LxF+UYXD6Rz%aM7KjxH~nt|dd%f-J=5lx6Fg+R6p(h9@2Dn)yr9W+i;@oeQev zqE)rXGH%is5}jsqxXF z)C7N%{BUaT)`!&T8#vpF)=j*}-(01L7Hn{p3c82C9*Pxpdbk(yTk4#^dwq43%qO7| z$4Dbo`_r39rw2DM%+1JES^iFv7~0T@A?`Lw)CKh)zN$D`xX3X_BEE<@1EHjLF=r~R z*0Ct61)dQvZhPhi_j`G=;D7J)eJmnIMJ}HyUzrWe-=a$b9d%vEKTQ?7=_x!zF5#aD zRXChp#|Kj_FCsw!g{#+4oEm%`U2?jK^3v3G6bIMg1nP-lEGC_AV1FFTj6XYTDDNRU zQuOIfr$$rN`E+VLRpUC5y3BQwClY#r47*9l@*rI%EC*8`eGMN!%lkih6IYf}E$Iz> zRxT8B{yPiFC&L^1ui@E$VjtIaeBmEtIQu$MoA~nCn|OYStY5p1XVWj^TTLn-cmthr zT%ka=&rsl9KBVaCO?+qR8h*5iU--B3Gpk8RbcjR}^nHVr`D>r*j}rSo`}UCNFSr_# o_J#^}q?cl^gtRXQWUY9WGLj?|S)I(En|#K2(t*En*MheH0zdDwwEzGB literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d97125b7f47d534547d70c8026028d6767010ec9 GIT binary patch literal 2576 zcmb_e-%}e^6#i}!SXdSa0a~G=qEYh0x&^gb)2a|#jD)0;6ird%l3c>lWjAhiQQmy@ z5AnTH2cwLRPj<$?spEL=?gEJk4|U4JUiR)i-#OZZeOQ!~`2ZSC=zqu%vb-$p{1;Yt@uylT5VLi*h?lX+VYnow{bcZk7wsy!cm*3M4G}X||hConD){e8mceu^X8YlC?o4Tnx z?=yrG$xVidc!N9HZiuBsa;twtuIcFeToo=`)u4_$FjD#iXd(mfSfiR@|RO6zk6;kNi zGTy{>1w$Afrp9`gA<;LGJ}Qy$9)oh4p|7!9o$mc4ll_~#4XnugoZ&DIcZlN|b)GhC44x7BCp6WzQE4#*MRS!jSXI{|uZ^7w8i=n_+g0UEtr7CjYOK^T{C+1q;#GlA7Ap$Mxv&ng|8 zT}nmTc;rbcId}(1p}77Gt3wF_dcK zorg!bkxxIyxY%vugG&ApZlsT~BH$TZdXh@Vo?uc!hLWUN5#UWCO#e~Bsn~4nPD$h? zSRge72j+<>abP-@`yFd*Sxrt0-Sn+F$qNiB>4w`mkCh(jkLilM|jZ2H=b8w;efVfIyOkF LL=k*TYY6OLwTaJV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..23270ceaa4ec0411b422b94214e9605a34556ab6 GIT binary patch literal 160 zcmbPjnpRYmU22k;Vw#>=lv$Q)WRhQOY?NhUQYjF0-{V5Wl^2Zssc}pHE?fEj3nTO7 z%)H`~qSD;dypq%u|I(6z(h^323yKFm?Pg&1`8W5{wiEJxf<3vZC5b7CC5bMXMT`Oy zG@Xw>Nwu^+sr9MYVd4piIR!=e1*t_Pm7xXcMTse?L8%4#MI|t^Hf=dp`QY9h;{suFw-x)_vPH@Jm;L}-23l;fBXgDCVmpQT9~gHW+`8E zjD5EdFE}-;P&Dmo&5={_#%NlB5a_QswtaAzHg&6#&U!Wq!U7YvQ%M)?lHAd2)wFL# zK(4VT(+kp)j_%6R$%%G>o))hHBQ5h*779A~Kr=Cct`lkk9VI)fo2J0EL~d6<&^?0p zw|8aH%_Pq*sZh})5Q|q`-8G6?Q?FJRt+FkU$^}Lu%O*h?wnb6Xc- zcU1IaAc8gw3$(dAMwQs)F39X@eJPx1!LGT|S+poCR#6IQXITq$7Z}&eZJ0C!fyuy} zokv%}6++=CvNgw%mMd^Qk!x1gLS1DSmG2|OyYV5eG6B+M$RLn`L_nnlE;XQCN4KhF z+u1u6WQLO#C0%nJ@^vjNOINSgSYVg{n`xbFQLbWd%1ps^fle+b3xptB*wh!PPB$&5v)S8`9ymGph8i-Mcf1+1|XA_C(9T04)mid*fZB-AYnPd3_g;|6VDSb3Qa)yJCxRJ#=>ea_rD7hFyg{uMjk8pm8su`5 zDjiu)=k2n)Cd-nfRFqVjp?J}eblo)Cquy7ADcVSEH66{nj$u_Y+=l9&eju$ZNzu{G zPAro0D~?_=<*He$7}l!oxV*kZ8VkY}xJAVdOyhYyQ0J6k^@dus03EE5mDEG1iT$K_+vSWpl&Z7By=R^?vcmvcB)zs*4sqS@o1j+U~t;@q=*#tKC-WG&XIDcs=c5f*BS_R;v4bS-u>=0IRX01xrNj}& z8PMaZ`n)^8md2+6?9T_0?kx6x`8`!otc`5(Frg5LlD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..53bbaf125e14ed9b6fc905420d2e95ac4776a4b9 GIT binary patch literal 2646 zcmb7GZFdt@5PohF*fd@GvOuK*QZzuCwn@`cKm$k{0c+Eu1d9bVZZ|jSmSneXHpTk- z*&pI}1Wze)^!Q26@i%ci&g^d5lr9|ka&zzO+?i))o|&6}{{8(=0B7-&z^U>~%dA&Q zhGVX| z2Y6$7RT^%7c&|u>iUR_D$)>BjrctQt&E~9CwFT0}&`4ypPNq!TqNoGiyns?JlZE+$ zfVe{-E}E9$i4HXA*D1jIT1!C@s*(5Z@S7dglz7$SR*>1VgnYAb@mLUbSy{HAc%WT)rZIn0! zUWgk zL<}mt7iMV9PvcBCP9Y_*Z!$BLIUh%wvcz5lF8sfm?e(N$6k}{Q!+D7CLj7>Uw1Zq^ zDsoL)6=@k8V-4NXYqCM{#4$lAa+w^XI4ck_Da1pm5ZD68+JU={ZZ)g6)7a^#JXgJk z8k31s^2j_dUEL$8z#tjyOxpG*Dt6zCeWKu^K%Du=3@4Mj(0D>3+GV_-z$IJ}$i1qf z{xF%@a3v3g>59Ha#8NOrZOl!w4MbohB=Ijpt>Ob*O`w2lDtgh^MX`SVR_`u%>X)4d<$aCrBH7^Hj@F`vL37JS3oajEziRGT?`LT!~~igk4O@eUR_vYIW~ zRrjW>N+P-;sWbyg!;v&E+x_hghH{QRr0#ZHExFuld7dN6x^$W}o#MF7T&l#eM05%> zBXExD8k(_}HKXS~&%}=HqK)rg!IvsN#Uc~&E1F)fZ(70f3p3NNLujl#;3^3ikSa>J z#T2N~uD(>NY0;{O2xQovjuqAM~lK|}(W0BpgvF_K@rn*0v$6tRe zH{o^>))tMK0`&n;RGY}Ks5_>2#UqdyIZnY~jd4dSvu5-u#u$0^iU&9K|s@ z0o^!`x9A)1PS7qwdwObVWm^rh#5|f}Tl1sZ(8Q>Aaw4t0^8~|DXd_RMjpFvGHcs*! z$tQjJlqY{c7bDo9l`FRoadOq=ff%4O8*PJ_!Z{BFyFO3f*!3{|vQ{^{E~dpM-erS+ z6l#FjH}EaV_mkuTd0(mpGtwUgQ%TF1S8j`o~pE(?)0xN_1f z9&lLu;t!PP$iMy+#*!9EKSq@uvO4`22-FLKhtxx?{EdEEH_`ZuF3dfHwgr3cDOQ*0 vs`Cgd>EE!v3-Y1o=uwEgQG8E!H446qeiK9%Kac(RfzC0K_TWd_MWFl#8%w<% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..77e443d2c8459c6103677221616885cbae4f845b GIT binary patch literal 2570 zcmb7GYj+bx7=9)xY)Q5k!bQtP0RtpyF|YlbhmCcXw^U9 z5Aiz}PpNYB_({(3H*q{ZZ#HQYx^Uz}W@cyJd9Tm&PX77#_dfxg!A}YY%To=fwp6lx zXEi9N%YMTx+cmG=@b#zxrLd>!d*1!Ee9dyJ`9gSNAg(at`PIDbE$L;eQO~!ecs+1d zbpEP#wQmJ_Y4gSog}yFZ^4)FCP>6F!VQ(f|yldUJLZjCf?`k`kFp)q~VK7|}tiZ7g zHLG5qb}ODjt{CZxuGDzX@m#vylATa6%4KSpEhwm43Omm^t`l5PIGNdQar;hoUSW5- zs)K@6tCgHUPx`*KrZASt-i~&nl@FWpb;{@47a7hT>`fqseI|Azl|T>PQ0NJkow~x| zVppoV%fcY~%U&bU{{M1&fV?>Hd>h2yXAB;AI} zn1Q3LClhmH)1rxYkd&~}2^^EyPDNJfWeGBHk`YMW3Ju@aZlG{6Q|tue)g~}o=-3{A zL`WvG$R&}%umneN|F7W24ZNE`UP6dDbmLIli@>+sdd2fsw&2dPzJ<4?tmCAPAf?nq z>#Po}CJGgX2=2}>Hjh}>!52|);C+Q&0h~aXD!Z7`+@#45@lgUFfC@%mmE(})Q)_`1 z+DY(u94g~qSk}r9c;JU78LeJLB z?EbXvYj(6Yft$=PkMbaMyW?sp@Ex~0A&tWH^{PkGd%mOVy|~Wavs!CZ9hVusquu*9~{`kSaoztE_fn8!_V*iqQm356Edv-4u+p2)JD?XbcgeERh} zunTV&eO*=9wb{u!)}(MxflQJ1>JcvUmhV`LHGM+{g@K~u>bb_sqV`)mL)XJ1%{z66 zB9pG`3Hv>l% z9ya5hhz}-&Wx(Li7{DOsge02P=W8tw3CDn67ZB$Sz{Cdj&*g?kp5V=g{8ngiJi=2T zi6I=|EUSY!#4}kP<|@WD`xr;jbx-Va9v5A+BdH@}xu-a`kjt&(_!FG?4I^>f97&y$ z?R&1mFT}hLPq{0@A)A5|p;jZ%|4;|k#EfI!}*J>x`3bG8jLOY~OyXx|Gd)eFa z8A>%7!S*-B9UiqQl<(i$H3PBb3Of#SWLsfSnoWvOuQIT&83r!N7e8Xi8pSn+@j^?4 zRqnc$6N<$k;JXY9MsdCO1CbXx+rqTTzD@S#p<_kEFfwDx#!90U@ zSYg8c%bm|BMHYMWwU;0?yn_oW7VvHo_^2)o@8OKRc%NakAp72-iIW|@B%PU-zZKpR zTY`9D3u?_&!43ou#i1f%DK#ngDPz6E)d~YrcSR~Mx8p`F$E$OXx5PHlYY?Q$e6KhR z7pbw&>z&NDOOf&Wv6yJybnqrgKtWZ*C0v$@{ged%@}U)6drE^zBYGAqHMp$eIzH3z zAu5ssHIn%MY(9plf*Uj?sO=hVBClcqFiZKj&zM3nrBrK27_Mn3GX!?IDEqux8)*-oa2cb(e{Q2(vuO=w*5n$@1)<_ zvge5)hDnfCRm+b8TU>Qqfzwo0AI-gNPL(17az8=AAnmFIjBW;KCemP~^;`NfXwT5D zVjrWm(r?K9LSGEu(W=o7vK@yWpG_d2dQ~Zk^qPsi%KGF&=>XG>68XRSJ5FVBpDr_K z(kh?p?+Q4R1kCkOvS-qOOJpqr&X)FI=$W4}SlY+i_a9)cR(^~zdAg5_joL%ZmG|(0 z4A1aAt;B%QCzw#MKm`&H(|8le(l067}$maIu4Kz&aQP7_MfU{YWD6Q8WmYgZ3& zy`g8QgcTWHn$tf~YF=^efi863jor Xs6sr*fKyzF{50*~5DXi%XTbggOpX@F literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..31b8b2ed282a5b839a5018f985be0bf3c2eca65f GIT binary patch literal 3931 zcmb_f`F9&v75<*BM7BI$qHImm00p&?Ww(l5C@oG?93@ReWIK&q7bk$2NKf)4;~6m; zxtOhmmZj|bzAwLr6cR%Z%TM$i{-z$_yECK3@wlhq#OL_UTkgB>yZ3$f-lu>6=dXVU z@Lv30;i1auwq@7KRnKbrl}yEJJC&;Kw%VSa$#iir>soim9)-tbN5eB~w$59QuRX`K^HtkywVI~C zmM=Oj-)paH$2YIqy1Ox{FuDz{!qIKx?eLRGDeSGe1=F?_9?q7onHy$^e(CBpUG?WC zcTO2FFe>NtJ3?W&>k5U5Ov^WYt6C7eMW^m61Lf&@zU%WbmVZj&c#K7TdpkC+DD2BL zv@dSxJ7zXJc_kioba4W!MOgXH#yJB=aDNH|IBMW9?n@zoV+sj>&1w-%=`F6J$0Qms zR&m?Ba=B!aCMl$MqE;BGh~Cf$h4JtjGp=VfERsdxiMLCkF7sOFwcXTS5)UcdM{b1> zLY{Sns5l$ANAS@J)P<21R?h@HD z(eq@8IEm8=LqceYDXQFtE0`ox&){qd1w5lL^H$yq^WN!AUrYX)ubEBKB8l_N01{cMvV|u5?2kZ;<6C3rZ9W= zs*|YSkyt}t$2vf=;93d|SOyN_kX-Y1mZTnGcckwm))fYMvj$v=8wX8=`!iOz-Z2UE zr)dKoJ}%af=Lh>4&`T^+$fpcc!UI2}a8BHJXBzB5 zWeH?qGn-c4-)!nuz7s@6$*fh~^j+^6%MQZnb0k1=Qdk6bd(=pF-BuezUfwff zq?mjOUrynR_zIJGOZ#WEts4x^fG(}G^}0MPHnhFjUqB3e4F`mVUsrhg?fW3C2i=^| zDU;t+m^5p(qO;*vgUdeey4U&OiQZ#pEP1R|*E%J-62s5S_tYcb#&=Tq7QSoXtHSvN zzRze|H?Qkm+`&-GN}4*c(~4y`c28uQg1Nn7M)Py`)SMsVCt}V|`GhL4_{ckmj<#1! z&ywfDvNYso$#V2!d;O~RqM1?F&+7FBvl)$#Tne-0iq*2XW!`aIDX^|X9vel6&+j0I zX%5HFI#oX0nZJ44Yc6wSs_eEIt&CKS88Roq|GgMOVj?A_3z&En`*DEZl;k)`yh1X5 z4ZckH3S|3pj!t2adu4VD4=m=Urf=gNf8N%Tb3omaUzmj^YEs zQS?RkfRmIFsr|VvCLoF(fdIF-)`fD+-N6{g*?PdaJ3?m)p3z)-HvN%uF8yTsV`aHYp#ZBxB=#WPBw`2C=HF1gz%%vhXso6Oa<_3! z4iRzwUWmjCk=E=@*#E*lwzu$!e{#cxH;~?ff8ljFtK9m+Yp`>F!l$>vJn$yQl4wvN z%VDJOa6l2dcMG3gy@{8%@M>@>mzfGjx#Pq=LEmMEbCbA8&o8p8b#?q5Ku{aHG1HD+URD22Uc*KyX?vlY`WXQu+0 zINBA49Jib;I7Pi|)@#{Nii$jIRcFs?Tf3&Gi_IOo6vkvn*)@w5owaOFyS7=$7Aj_~ zRyDol?6h6;-1@4vJ#(p|n;YW_-CN))9NO}}6@DBEh22GG%B)lr?oa2gm}_Q$es1ZC zE_mYu+m{R&=$3O@9ihve670Qk)#PfM- zn3+;gmlS$Z1>&2xJU!vM=DNb+7ErD(W#^recR`o5tL=j3_T48f+wx8+9F4H3WpBgA ziwb*EW$lR@T8GK!odV$IIIx!maQ7mN9|O#lO|C60yUtbNaU6pNQs@>B9wUT`HrMnf z)te9uWCK0N+nJ@1f!TME_72you4R`?uDPnOI_}EwlH+(CctGJ;B>#4bWE>CD!4lx9 zx=UVp3Xi38t?7N%PxX*O+rtx?I1l4pU3duZR@gN*d}8>a4xA)AnzmWiQZ_npnkYtwM+C*=3T+lm+?S4oox-$4 z zQWU6i3od_=sdfSL3A_&r3L|e8+n{hyu6tSv@I=w9lGkxC>AR&&G=V4#MwE-4P#btZ z787_9mkjJdvXi!dKp`CoB%;X0@j(TnVPQ}rA89>4FmfQe{zNFGKRyIAfjpK1#Z9{m z6fq|sSVs<|3a)0WX%sBKpyo&TA$_@p!}Fdb5fhfBcul)C*5?jbLXu(!R>+emH3|IGj;jXty6OP3Uv@d?yY0f`QfGz%MGC75Cj*vNxf!U9$#&F1H3bBhYok$vI+ID~BeGG6S&3wViFhbZKQ zh^vq;#_?5l!l2GHT+jOPY|yZFx`0Um-qObdUfwZdr15wV_$IqbO?#(xMVFa} zK3!^ON+r1jt?9~oYqK-(9qbbteplhqx9)?WF*Vb_(bB)KFklvo)ApKE@PmEMaaMRa z3-6AnExA}0mK)cJd;~u)-BFMH5I;)b2l%mpZwu#R_$jk()m+g#xPz&dmWp(IyA=yg zrZT}!Su3o?+KIm_0w_0=WqhAZ7{ zm4%fVvl_nlUkIw@MXP3U%Y0a=;%;# zY&4TR@hVvQlaIWLiFQ0Wl$_-H6xYuL>u3G-Yuwm|-*J?>xr?S8MyC(r0N+Jv1H=iO z^Fh!{&7)6JMx^#;Hjv2--o!8`BiCg32hqi-ss}@y{hmU-Q2*ttigODGS?0xuFJi<% zsHsbg$YsvYIGVYECz5S1qk{@AU%QT{a)YmZV8&!;$-fD2LBL&7ch(Pgq_f71CIJ}d^*UPFB` z*_OG9kH~={&R;;G_*8g8`Z}Ke7xr+xflvIC8_vIuinKB^%IHx&wP6* w^mDu%iuQ*9yEBWizl5^C4#e8=N9ssYQx~7T%pTF%$ETba<7y8?vJIX81Fl4!egFUf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7e0d5b94b490ade340f5d4848f0e09038ba4b262 GIT binary patch literal 4065 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1820db593dafa632848d1b5f04457c6d8ad51be0 GIT binary patch literal 17 TcmZQ>-F2ze;>qvL3{U_7LOcaz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..35a038769b15c0935bb3cd038f5cc1de7579f128 GIT binary patch literal 2 JcmZQ%0000400IC2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2f343a18fdb56ac3beb4b3a2153222e205487d2d GIT binary patch literal 4718 zcmb_fiF*^*5&u0~ShBo65JzGX0%@Hf%f`auu!%{iZNOHM?bdQk4YcZ7dXg7QyJB}` zs-{PJByD=P^hneDNbiF{0wy#^uh6FV|C6un%496WQ*Di+EIds1dePo?vQRWjwEA3|VL z&NPgbax|~%x#+li;zzSUzhUO0X(J<-)RGnTsEFOx3Nkt&b!n=$%v3ox2<&B!oT+B= zGOFpeG<7u}P3Kk1DysHUG_G5=St>}~Ru}WK%IFseH)6EXrdG7AXwrGkm|9NL{b(hP zQz^swrRK+3__SJ(0-?kibwzc}NZO{R=SBn|a93)Ked0<<)jJ@74uQ=%X}i+v$r5)eK^{gQfNi@s_naz_N z&Cn@tC=N_ZKKG|mBw=b?K%69td(tYIqSlUtUWZnGt9?r0JUdkif2RjTq0K^*g=u0vmhS=k+?o!o8;&o8+Z8>uizE z(F!9uD}uftMZRA_7`;LGFd*Qwmo$rFmw1Cv)TbR;*^x3zR3L1fN_N+$uV)XZ7|VBm z3Up=+R;Q9;gz0Tp$4qlTX%4B~HnhlcpaJP~=wBd`Zq^1-6Fc zwQ@Wna>5}z7{mxD0=pZg#LfKY$2-UzcI$Y_q)^)ev*AQ7^h`J~oDEXWQ6g- zSC%H!6)!ZPO&cu#NNDUKfZE9qjEjm&}{B1Uk&0szNX+l+|R-N1`RK3X}Ydg z`teO_?-Wl3-{MMm3g33RXLZV=Iwq5=tyjT!@j15UdjeZ(s(KgIuTHYz5 zUr++Xr;~HD4^JJPo*SKu&rGLgrV|SSaVPvY&Yy4L5=!h};n%JBC4NJ*Pb1CK4Tf5o z@#6w@X%})Tt|usYH2hZb^0pbnalMF3K|F(JsRvuqJ|gonM2=+o>Gh6@9tT5fScDW zjaWu?-QccD*Ac?Ub^7LWAWYKF&ZByoz7}bqqD)vaXgCihjS>|r6{^G_Wt9)jXh8r$ zv~+Fb4=38Y&LbH*@fYXWPW$u)RNdcN-4Aky1G=8T+tEYMAcLaALn%wo4w`4Xu#wik z(yuOh(#IZr7qRIAJ$c%_qtR)1<&;KvkM|+c(nlN$Rfw zA&dC04KeI>?rNhi?!BbB;Q7Xrb&l+1`ok=1sy{Roi-ZntSjFM1IO@ZR{?M^le`tdK zC+XmwEtP{%;ws+NjOJB5?7Xm&P52wF4A&dze=CV)b+*zwi?67281s%gg2y6Hi9=!hcj6A_{TgI zc114ZY^dpJ1W3TS3l~vN^j$|M@2=vb3yDi8_g%)vUG>;7g0+s_;rL<@%`i1@YDja{^$~(j692<)~USt zHFWrKo&-{__5|MHq!hV&6+d6Nh~KW_Ip~kZcoAX$b#=VkMy(xx}zCM+(yad(5IFYUXw z{Wto%#g3(Q+UZYqrhk*u>AAa02qo6BBQxyH-o58L=X~dN|N8sa-vRXFdxj&~l3^}p zvIEOj-ndz~=?xFqh5X=f@A56TxUx2^fH9n~oq}F)_@XIvUNm&W@`Ph?Q!hI9vdDS* zgvniZimwQUXlj|Sa^2)s zK~H;*VHHLgz_2?rCXEhIGWAdy+6ux8@U|he#kzK4Xi!k!fEqM0G-ficQLwmIa!BFP zoZvm#p3_;;KQttI`cCz5KEzM;4-V$VKz}ZKx*jbIO{6Pf69txciMvZ0ngh5t2L1e* zg{#vu6AS4Jsf!D@4AV@nxt>_5{OJ>ZGGLRaeG31b8K-1ra;EK)g}n;)OZO|c7@9j> zk9$VWXD(^wZH9PC(x0;z#S$;M6gt~pUDF+rHz`_%ZRzE+#Hf^fELoX8;UO|jM~{k==w&z-t6(RD-?`H@C!IK@ z;sE-?YT6N-Co!R5kkV8VlqfmGxyLXQON9~jOJm%R?h}<)F=ywNhSL~s#1PITn>QCercG&Ir$+?DcPBG1oy;do%l7Hp+wvl zyIv91uv^M{RElPa+N@iTt5gov#2E%DJpUDe9p&k(_3MbHLq+rhy$TjIT)~_y-**}M zo*i1jd;hAffX=Eu%G%1R;K*rs5l3Vssh1z#%E=vFOF@Ae1O-onfo>H`SY|lXNi2Cz zX@zP`EN&xMgNbX>n-zw=l|c@RE~sx?0u>qRc+fa|p1tQea2v=GvTHt2xXLgt=?jN% zd6I6KN}Zqjxtz-0T2Z+AW}V1x<>W-svz?2E=~vAAlmN-8^z6))$qQ4nqqE8Bsm%0L zYMvqKhyOV!p)CFYA2whOAJG)5W_eC_gd+Er^kfCy3O-@j6ZDr&>ZxzsiT$6vd~VLj zxZc61DsJO5>NYg=Y0_HWoE#7g4e70$LnJ}1N#0m&s+BqJ7(8o=S@|HhQidg_N-J66 zggd*byXJ08@?!X|B`r%heu@k7T20#}3LS+_po6k5Lc?ko>Y5M9iwnJfLF20ijrh`E z8)%}8;P1d_& zcNNzu1xgDorH>R!>HD3&AJpNbrkoajD(CcX;`H>+?5->mg#(=aGQ0QA%$>R4z4yEG zpa1^jUjUBbb%wiq9t4(C^-E(H*K6hKnpd{HmCEp#ajCwte#}}6t|(v(Y1gY}tDaG@ zdDe2Qz%p#>iV;|@lPyLTil%4P0|g!LT^(@GG3=}_=lyKBZqBo+mZM+?!=6&fm6BoS zZNv9xjaAN|7cLr?jjU}r)od~F=#JwIVCXKLY*=#Ahfs5LSK5|W*Zt5 zs7WNy!_ZYK`Bv32f`&&2-Z^3(HI9z(qocWD8hCDH`1t)pV48Ot-TlJ7?Iv>RfeY<#)Nlf8m1o6~ zJ{_5jH;!w#19zqf8WTjw0ztix+K6xwYuoGV`>2xv03 zg|3Q$4$5ZAjC7N+KAOLMb@C59;(ux7n}^g}3=_v3*ij)PEd zYt$4aTQ`DQw!-P(llS+tA*hq@CGI2 zqe4e~n-aRbySnN+Di(xtr>H2?FLLj)Wzx#?La7kBh?Ns{OU2_tqYAG`(_Hr%p-o1o z%=3!ntN5JI5h=@)C#vMV3RCF1B70}uvWgX<(a2u8;ad*(eW|sm83+a{N=lsJfgvN! zu%Bq46~E>bWo1jDg=wH*m9P**CQ_43ZsU+JcQm-Dr{LfThT-?)jj+$eML1D0>az66 zY`0*}=0z9=+vM!6Zmpq#%P9m{({LL;Ci)Tajcl?Xg!dJEfkBHs47xapUMFToZQ&ob=mH$83yJmz`XV@ks`k-l(qp^1LdMbs+FTF|zN z=P3g$$Fv(2%CWBzd9}00FiP=yzmSN++SYI6J;p^wb!+$vo)ewto79g#yb%T8W;lE^ zl5O1+z3n?GT*XToZpX((5Wh<;WsCQ3YPbr%M~#i*rs4ZYr|>F%z;K{{i~Mf}2VSFRNIRl5n`}ST z>{z^_;I|CB!UByQpbB{={ZZoBw##fm^nv@MB$oq1}o;>qb!ZY>&~Wt45cAO>=)K>Re@SS@oe(sUC&nU`jadL*ZcZi6~Ab!Co2 z6s%5qi|oXX-UH&PN%Eg5F%#&*U*tDQdU`W9^?FmE5>x>6zJUEWK;M+eD%z7Idm6fF z{3}Vl^reR|IybQEHTsH|&z^R(d*fMpfkk`M^i9x?6gF{JA@g_i|4r@}D>Z2~iBIAl zsXGj@noeZ<$s18zZ^>PHX66VR)CY5!H!z%le)MgO=%X8Wa1$qwoFCNlxr~154NP@_ zgtH_RVqu1axmY+y!h9?gNq8(47D@PYEG&_5Ar_WN_-rhcNHAidOhP3VI0@BQsF83{ z3c|?*gu|l**A8;{Zu$8%i^ z-cDSC9f5n2tP5}tXKujL6W5?-Hu3q_HgKgd_zrdn?IxaCDqP2v!5jFpFviHNh;O3j zUF=ctB!!&lE3$WiKo^0Y%jkppSBn`vufIU!Svk(g@thosa$J<-k{p-iSdwE|j$Dp4 z8U+9%x2VvggAU1z1@`LS_$NlD>DJ$R6IYk?1SQDJ;+kSPJ?Nw5`RHEh4ZQMi?56Ps ze)u+Rn0^QPCVn#gCSF~ltv|nxS2C~T*V`QJdlx+lu987ww#eXZGP~%3u!-L+$wc~- NJe8)e)8Ps1`yU5)9?Ad! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..35a038769b15c0935bb3cd038f5cc1de7579f128 GIT binary patch literal 2 JcmZQ%0000400IC2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1d94e09dde20cad4c0b9095568ad6968861abcb4 GIT binary patch literal 4439 zcmb_f`Bxj)75*N`7KmZ7z)oz(iPPd(2x1}37UMXdXkL zv`yTmTe>FQ_dVSg7bifQ)8(h;oc>KYZSS2K2@!^zxW~WDd-LYLyWj8L`S*YR`gZ_> z*b+ErOUKcSqMaSNRasstu3JmGRmdkt)Rj`9GN`RP&vj?5vXRZ{rd_sVvb(w)2tx>T z7A@0Ut0Z*QC?--KO&E;=eWq1Rq#-TUk@?z<0|ICHM$uC9 zx=d(>BP~PK6FFVA?UL#&CngQsvC6B`aMUGT)-HwxVnG+}oTZfpYR<}>Q56o{s8scWidM#izoBVz&(IF-Fv*7Ur`CA*(1n$RZDRg{h=XBX$L zSkGSG3R<8vqXAKYmTcD6iiYZtdj(F87KVpLM}~(6`_(6wMh26k!}+0Pa%eD@9638U z(w}U?VSzT1l`_dmhI3W5mlbq*>>d;txODF3_1U?po0*C9<(uo8u4gK?BUkIU)u5m8 z+%3>wJ$bij5XNzVgLyNh>bk&Vu{slHmu|_NGgfc4f28099u{cbl~4lBwUiR*=(Ziz z(QVO4-L){#x z-8u}!dTs=pcueyz37Wjx;(C+2k{6(NC12a3ce|vq%6#P6=?SY-(4-FMuHP6md67FwVzdCj1pfqHdVGL0FJ#R-7CeFTtN@`=gGNj`cPe>)*)rD|F2ZS%L<0^Fh}?*Nz$ddChK$0M>_@Y^T?+N zZFa#*Elv+r9q3r9VHZqmbq8$6nExXzX_|M}$W@%48Mol3rK7rqByf~i*N36@iL#|* z$GM)&Fy;lCILo;TiRCW6+(Sy=1zeBd8GJw>`A#|F!R*C~Be`>o=hYIqJPfMjRxZ7o zA_BbuCAgo~3U1LmLD>Xc&!f+I&G}-v@e@Wv>IJfL&n?N=o1)sgIM*kE)i{K~txq=^XT8Ho|71XM_BKMt<8e@z^ z{d=vLHv{kZrU|IKTfcukc3;i;4Svh!{EpfQ4bMF9I18#?rkSTXvonE*Qq+5R45A?D z3#z5@05Q)GO3k!p$eHr$lC=DjR%)NQm1(u)zaO3Rl5#<_HM%lx7$)~u5=Q_%Y0!U< z3t^I8+b4`1{V~zM24%vsNfTxyW0t8{sZb@7lvN=#;vkw3!NHDW{L4URe_*slJO1dd zt#nSaf9?E8?L5N04(NCZ-8e;SghA20Ln%vZ8x3q7I7H7s(OU#Z<%QWSO7!u0V3}p@qBuO-uY@()NwrJilVu{ zCg42LFjZ%K8&~4JTbQIx`VK$-K{|xcOr9C1-3Af0L;RN=6{ain;A0w|=!IR@<8x|Z zRnu{?iCI=cxT+|$?pS@7_6O-?#{GVRSa@C^V7_z~b(Pt|``@NZ6IyG!(EVvrp(BJj zWc>RVeHBi}H?bIPcoj{=AbV#6YP$DLwDajUii_#HPU21l{g|;v%#E^Qg z&+q{^MaaF|`0C;YzPXL>yHe>h;gnBwf`~ZKhkA+Uk6fx>aJ+x%+CxOY!moYO9kUlh nuluyW_sAOY8u5q{r-x`gNfp67AEotmT0?aD2sUZgfMfpyb?x^0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2ccddf81143c6ca32171840d02956cf6d49c85fa GIT binary patch literal 4649 zcmb_f`Fj(`8GgqWNR}64#1SANkk$lSHkL19z=Q-07}v7hA_r=qRoBu;UcBB_c2{L z5;*8e&oiu|n;pEcG&@sVbY@JaknbJT=Sqd80b|j7rY-A~t!&P;-LfNl+bXO6D1^Yq zqGQ_&ODR*gimBn?Ocad*UA9w9F~dla8gEshp|1Zb|oMQ=^vaIpuk2dHRegs~4jJ?e#9YImak@ZYtx?la5g|tSFi& zaW-rFU%EN0h)?MAQXrAOpfBiw8X3=_MTP_*useISY?%3gO7>2sh@nMbQ&D;WpH-AM zwRhZ3TSGInCNv-+(45V>M$ytenpa>~Z~y+j0ok*^cP8Jz@4(D~-oe3vfj)U)aNvOK znJMToY!+xCuVI@;vb>YJJF8(+KyIr*-?4+|&rF^gJD)k8K5>50FwM-8>&f}rWyR^; zfw={GDm!-@22ngHur_ZG>!vC2XnT!{lQS1&&Ks)LI^5E*0}l(VTNO|OP1TSRXl-*n z-7|8&YeubtEs#vt4JBDH$;PlPh8^9g&eCPkY?c(phXur0nyD>E`7(wlk2sFLB(S4K zPDd6}8N1+}k_G8VD<^q{Yaz@hZLI&US1sd7u! zE;HGbsydCZIu$}yRFEoW!Kc9b!0~OBL~10QKn+BMt@Kv6TT&@haF|<*wlmi~W7}Q~ z2L$%k_2r!ukAea=D|-x=9s0-KGTm<&toonln9mX+5_PDY`gBpl}I}JOuMR#77q?Ko!E$yAxExjlel*KSgDSEqm zl@wzF4F*-*xB^8}vyQ!}@Ggo83Re&4>LFwmSlyG-EJ-Jd57IH@7x+1V(dr>z>$@dg z1HMBYr!|~HCXNqbO2cD#ScNE~eyb_<1zDRsAzW+taBzH@aJdQ>tAX}V#l4=RTW-O2 z=2xo4kfOg+aXP0~AEl{Oer0^b7?+;zBZ0s+lCBMs>Mur9>xyv$o>9yQ#8jFp7$M13 za`~IgzdU3dITQqX-z|cJI&pN#ldAL`$?GMWJc?O@p2Ci*AOf9r0_{#xYq)^9I1HE? z-iPoN8kX>kimT7Fh~7KtD86`0WeCh#S62n&&&P2QFKBoadz44N zB=F!$Y~EI!qWH2vBWoj~F&na!bA+WjD*c2RR3%?V6+q== z$+MjkhUsU{j~M`?6Pc+~PmLd+m^v~wIysS@oJbQtMt%3++AqEbCXD8v;%80x34YFI zqn_lsI!&m|NAXKm!T@I~rYC+p*ZyAO^1dmf{Q4{WI*wQH8`e&?2FIi+i_Aj}?bxj% z?e_vt+`XDXg{@|IrJ(##phM5+N38`r=lk=dZO^d_3z7bqp}O|mY!Gb%**YP3_P#3p zXIzToPk2?s?^IYvaD@pqug}Rl_H&Kx3UqpI*J8@9dnZ&)KwsU1g!@DHRh?^CR_a`5 z$Z0Z;Zvo^coc*ZU>S(&I3 zy^N{|8^E=QA&#}J+tu@idpA@og!TA~Kd<9H-znAoE!F+FDr-RN3uwb`&T$2b{vC!a z=N7)iTCsuSoBV3!%*Pobm$C5@XZ22dpx)nYmA~wn!@GwmWrR00ETelO+1YgsJ+Jx{ z)RTCZ-^4akpFVzpTJ-aotavqJA6E@rv&{)<&HFloe@JV*D>2yDmDt~xOdPz9CmWGC zdL1Vkai%LV!u^xn|3I*x_V<-&4S0*A0_in0+zvEB39*IWifBcMeHizJ;OnKz`Vh~k zQyY`Zcq-X>4QIGGdr1v%QMw38wP2E~n>@50;(yA7QC{Jow%I}PLm9Ipq|~gGNGHWr zd_;*Rd{H#cO7}pNzj3{mJLmoXJ4hn%N1vk0y@W5!HDqs*C5ClXS$Kb(BD6;EG3ep_ zqoD|Ul2=hoG`xZs3Cv%*3_IQVHrA`%Wt6AWS73Ku#m58jZjvFvtmPeSjN)lV42yS2 zZ#TWDY0*_`;yoA}%q`G4UCE>0&J zlGpH}A4NRQH&}RlIONcN887`C8#rFY*KYE{*xN`f(nyffL!5UpZxqc(IRArlgu6TOPp%rU{r_+SI3xf7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1f450fb733e7d5c426766106f729d438b9d9d19a GIT binary patch literal 4621 zcmb_f`F9i775<(r7+Z{u5laFIDWN9VGC{UXFkm17v$>Y-7CEK{T6HWv$%Dr;>deU0 z>6R|EO`7iezNY&^Ai+*(nzTKqIj4V9PTTLzjASVmX=(XEdT-vmci&yU``tVL_~$Es z256X^G(@0)hX%MD#wnwu?Mb!RQNSQr>G=F7#U1Ljr#sqUOxv2%IL@hYwy&>#dh zmt4nLTuNDnT}q9FZ#1+D>~q{wD(@8JoKf*o(JmhM%>|h{D{bi-zAV&EtP>biCrYkS zuw=@#ed*eUmC9R&=amhAE;VX!A5C=?(I> z^z^QksAB{4cC;WNupyW8%#v;Rbfv&90%qU3UuBN4M#nE*6#GMQJUqLRuCC)*m%((?2e- zx2H}_*_rJ<=g*SV^=PD4iEP&1Ni#*Q^o_0CDFEL{x~0{7E` zFdQL;);3XaPk@%YB;MiPX&vvzd*YbHc^$(zs_-+#n=EN8%6eo+RIOt=+H z*RZ{!<1Vb01fp>)ly*_cWei(znT}CV&j!gmE`1|NN`dXAq~QYs zZ7O3_MpI-1p#_V)d=~jQ3@{-F-VQNgML4im=ArzfUPOpQ)XOO}|CIvzz*A??=!kN@{(hPADh>(xT?TY+AqP#CorojkiJ$3Evc^K6YGhCgMh zPCY*tqD>&z1cDdusnNg3AL95O{;1b=H(rmtkxbyKmBX1m~xt)iA)nP z8moV_K73Ejc?q|aIm@geZ1Yv?AR zww$Oe%t|*}>0r&w&yO4B=z0735R}uVXY$Ai+jdk@<#lA`QJddo0fk9EZai)0`9;TX zAtGVUVMUB*oeGnci7GKbRK?JW^=LyJ>$`TSj~lMNtk@PC@k%guaGhT%we@Ya^|&f< zK-V+qMUrD&kz#O$kmcCPmaYq%_V zmQLQr?wMrr7M@tfiI;J<6&Lm;&Z*@+)#cG(St-_nSNX3fdL4fcQkYU<8|O-7RfR#k zD^NkGPAIzgDEFvco0BWZCi|B0Bq#5^uKr%*mKfUUv2o7S2mMBfe<=rQJiM6rWWu0$g zvj&HFVd;+KZD&lBJ{`%#vBXo^Wa3=nBUu$<1uK@QXxqbdliId5@$pyji80>!soS_V zlW0jU<8uLSxSgLu(e{x@YR?Tk`*&>O|1Es!bsiXd1Bn%Ub?i2to8i%K+{ClV7xA42 zHMhKpP7T*6kTp3HcwYb=x_1TNo4J9Xtl*cyt$a=XiZ<=!CKc#SeU$l|VApjO?;C+V z+;kH!Mq5|So{7B_?Oh4Cwc>Tk*b&Lv#Z1}F=XY}4!|@T0f8iJ-1mA(ba@K+!{|1w5 BHQWFI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fd52c8526e8e1e8e5ef4568d105e9de5759270b0 GIT binary patch literal 2990 zcmb_e`%@cL6#g!cE-VWTfj(%hwl&%i+Agm`EmaFCrI7$iC>24|o8%@eT{dxdQ%rr; z=Zt@dzgOy1w4>uscE-P{<9P1w27-iYoifAT?7jDV=bZ1n_Rqh5`3=A@zGXP=2-h{N zf|DLytE^@U8+OLD^SOahe!G;f3>zEnT?LFGTC@v#!R9$r=!Rvu1~-kn+%<}po(d3B zS=%VN3L5`;+ZDFOP2KU%9o@e-XB!2>QqauMl};DEH*UsF?l`l2T`+``Yy1w^O>PzR zlxvfXV+>$uPfwH$Gv}kETc9#@7liBM?Lz2|_U^^dprEA{4d`HKOQ#*9U~#u>lfsiD z!=qxjzh7i7Uc8X$9~~LV5Aod4aPET04~g99NDDd{I!IT%Xt~02uXATrL&%5gW*EGB zY31hJ{Pap{GI?!f!!XTM#c{=Y?awOR=Y1|2`l~zp2OyHR%T_vT79Fy4K*147LhTkq zDB`#jjjYFC!pavJVo4dYY%wQRdD)@Z6^nN&x-Igi$a-a8AWhoM$*!m)iXtQP5AhCjE<-ZKBL&xDrhU0rB#C$_vi^ z%J0#eYV)+>0wi5BXfHWBZ6$3GI}YuT;TBh87t$S%3t&i!g7K z!f^>>Dqh27hJmLM;Fsn^#T7F9#&f(x9Y6u4N4rduZ9^FP>TtiG)f%o~T*aH1&~OaL zWeHymI?aDcas}5Iw5o@GiM(E`-gmOMeva0w8V%QxP%(wKeBf=rG$b)94`vt+N3yoy zuBaxpS5WBzI*2aUL^Wt|vUoxzvrL_FhFiQK)+t+B@D62gJsgG+iqq3Vv8Sj!{a)%* zQ?E$_-v9-a=5u&k7Tvql(4HMy0rihvCFS#4*I8LoD=L=pzJ?d^k_>5@;rLE+?&&oO zIMp;oQ9~AeDl*719E%v11Xucdk zbLg1-=%Dw{XyP=W4WD~!EA7+Q!}fl7dta3u4hVgR7jS}BRh9?Iv=W|%c6#2Uw-BxL zNra|N9K27foMv71P7hU2X^IB2QCb^FLd6zdNydJ{nIAm)FX*XxsI7>i*V~JsFOZ6p zH&Uv{lj;kf9gID~(WMy4>p$Z{Bc!4Rd`Zs(B+^X(JAgetV5kWr7!3d?=(Yqr6MKl) z!wuh~CANk5{Y^|J`yS(<+}*C2#DF52F@Zr)XHn#Ao Xr}9LDic@DTAI$b!)qU=S=9EW)9(;6K!2un3A=S~iI7kc&pUDB27*P5KH9 zS1%eCOuhsi*%>srZ$Q2SGy9w8N%N`uB}6iQRZBdU@fev9KmY**5I_I{1Q0*~0R#|0 z009ILKmY**5J2Ev1X}VTA@?!KwEsTDhC*b*a`-rIv$=s~{ke5%um2wyPSh;T?N61r zbzUrP&WH7{qwXN!^Y3zGJpON8RKC)={>psrcAN+x zfB*srAb;M1& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7c2e6cd733937d5dad1b5cbde0c06747337b1103 GIT binary patch literal 18497 zcmeI%u?c`c5Cp)(Py{s)z6b-0umx+eeDzRxOAwij_QIX?)`&73Poie3RU<%v009C7 z2oNAZfB*pk1PBlyK!5-N0tEgduw6sidrX;|Ed3s_blG12f-!F)K!5-N0t5&UAV7cs u0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjZREpP(ZnGxy$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7c2e6cd733937d5dad1b5cbde0c06747337b1103 GIT binary patch literal 18497 zcmeI%u?c`c5Cp)(Py{s)z6b-0umx+eeDzRxOAwij_QIX?)`&73Poie3RU<%v009C7 z2oNAZfB*pk1PBlyK!5-N0tEgduw6sidrX;|Ed3s_blG12f-!F)K!5-N0t5&UAV7cs u0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjZREpP(ZnGxy$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d634761c39e313131f1e509c17bbf802d3380c62 GIT binary patch literal 17 UcmZSX(>(KaV?)Ip1_)pQ06QQA#Q*>R literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..22fd28d4ec331a633f1a886c68979bdfafb8a4b3 GIT binary patch literal 9 KcmZQ%fC2yj5&!}K literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3cd8904965960f0e9b2027ee96b667f6fd328708 GIT binary patch literal 17 UcmZR6t$MobZ_-9r1_)pQ06U}vt^fc4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1fb92af3e79eef661361c24c27c0eb2596d5e8d0 GIT binary patch literal 465408 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%0DPesk^2 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cfe23c90ab34c19d2749f7055ab8fb6bd89919e4 GIT binary patch literal 26112 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 i&_8*`r-3T*UE + + + + + + \ 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