From b2c58c73b8a92e0bd10893e440cfb3575fa41fb5 Mon Sep 17 00:00:00 2001 From: zhaowenyuan Date: Mon, 6 May 2024 10:10:27 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 64 + Readme.md | 204 + chushang-centrale/chushang-canal/.gitignore | 64 + chushang-centrale/chushang-canal/pom.xml | 42 + .../chushang/canal/SanyiCanalApplication.java | 18 + .../canal/sync/SystemProjectConfigListen.java | 122 + .../src/main/resources/bootstrap.yml | 47 + .../src/main/resources/logback-nacos.xml | 131 + chushang-centrale/pom.xml | 24 + chushang-common/.gitignore | 64 + chushang-common/chushang-common-bom/pom.xml | 93 + chushang-common/chushang-common-canal/pom.xml | 33 + .../canal/annotation/CanalEventListener.java | 24 + .../canal/annotation/DeleteListenPoint.java | 45 + .../canal/annotation/InsertListenPoint.java | 45 + .../common/canal/annotation/ListenPoint.java | 48 + .../canal/annotation/UpdateListenPoint.java | 45 + .../canal/client/AbstractCanalClient.java | 108 + .../common/canal/client/CanalClient.java | 22 + .../common/canal/client/ListenerPoint.java | 25 + .../canal/client/SimpleCanalClient.java | 85 + .../AbstractBasicMessageTransponder.java | 158 + .../transfer/AbstractMessageTransponder.java | 157 + .../transfer/DefaultMessageTransponder.java | 68 + .../transfer/DefaultTransponderFactory.java | 17 + .../client/transfer/MessageTransponder.java | 10 + .../client/transfer/MessageTransponders.java | 9 + .../client/transfer/TransponderFactory.java | 22 + .../config/CanalClientAutoConfiguration.java | 43 + .../common/canal/config/CanalConfig.java | 200 + .../canal/event/CanalEventListener.java | 52 + .../chushang/common/canal/util/BeanUtil.java | 52 + .../main/resources/META-INF/spring.factories | 3 + chushang-common/chushang-common-core/pom.xml | 65 + .../common/core/cache/CacheNameSpase.java | 19 + .../core/config/JacksonConfiguration.java | 52 + .../common/core/constant/CacheConstants.java | 29 + .../common/core/constant/CharConstants.java | 16 + .../common/core/constant/CommonConstants.java | 41 + .../common/core/constant/Constants.java | 145 + .../core/constant/RegularConstants.java | 18 + .../core/constant/SecurityConstants.java | 63 + .../core/constant/ServiceNameConstants.java | 26 + .../common/core/constant/TokenConstants.java | 36 + .../core/constant/UnityLanguageConstants.java | 180 + .../common/core/constant/UserConstants.java | 68 + .../common/core/enums/AppStartType.java | 34 + .../core/exception/CheckedException.java | 46 + .../core/exception/InnerAuthException.java | 16 + .../core/exception/ResultException.java | 50 + .../core/exception/ServiceException.java | 73 + .../exception/auth/NotLoginException.java | 16 + .../auth/NotPermissionException.java | 23 + .../core/exception/auth/NotRoleException.java | 23 + .../core/exception/enums/ExceptionEnum.java | 27 + .../core/exception/utils/AssertUtil.java | 26 + .../core/handler/GlobalExceptionHandler.java | 144 + .../common/core/jackson/JacksonUtils.java | 49 + .../common/core/jackson/JavaTimeModule.java | 64 + .../chushang/common/core/text/CharsetKit.java | 87 + .../chushang/common/core/text/Convert.java | 1017 ++++ .../common/core/text/StrFormatter.java | 87 + .../chushang/common/core/text/ToolUtils.java | 319 ++ .../chushang/common/core/util/ClassUtils.java | 196 + .../common/core/util/CommonParam.java | 46 + .../common/core/util/ConcurrencyUtilTest.java | 63 + .../chushang/common/core/util/DateUtils.java | 327 ++ .../chushang/common/core/util/FileUtils.java | 256 + .../chushang/common/core/util/HexByte.java | 47 + .../chushang/common/core/util/IPUtils.java | 321 ++ .../chushang/common/core/util/IdUtils.java | 49 + .../chushang/common/core/util/JwtUtils.java | 123 + .../chushang/common/core/util/MD5Util.java | 127 + .../common/core/util/PackageUtil.java | 461 ++ .../common/core/util/RequestUtils.java | 50 + .../common/core/util/ServletUtils.java | 264 + .../common/core/util/SpringUtils.java | 88 + .../common/core/util/StringUtils.java | 537 ++ .../chushang/common/core/web/AjaxResult.java | 181 + .../chushang/common/core/web/EnumUtils.java | 31 + .../com/chushang/common/core/web/Result.java | 135 + .../chushang/common/core/web/TokenUtils.java | 36 + .../chushang/common/core/web/ValidList.java | 134 + .../chushang/common/core/web/WebUtils.java | 206 + .../chushang/common/core/xss/EscapeUtil.java | 155 + .../chushang/common/core/xss/HTMLFilter.java | 531 ++ .../chushang/common/core/xss/SQLFilter.java | 52 + .../com/chushang/common/core/xss/Xss.java | 27 + .../chushang/common/core/xss/XssFilter.java | 38 + .../xss/XssHttpServletRequestWrapper.java | 149 + .../common/core/xss/XssValidator.java | 35 + .../main/resources/META-INF/spring.factories | 3 + .../chushang-common-easy-es/pom.xml | 30 + .../com/chushang/common/ee/EasyEsConfig.java | 8 + .../main/resources/META-INF/spring.factories | 2 + chushang-common/chushang-common-excel/pom.xml | 24 + .../common/excel/listener/DataListener.java | 30 + .../common/excel/listener/ImportListener.java | 72 + .../common/excel/service/ExcelImporter.java | 8 + .../common/excel/utils/ExcelUtils.java | 177 + .../main/resources/META-INF/spring.factories | 1 + chushang-common/chushang-common-feign/pom.xml | 55 + .../annotation/EnableOnnFeignClients.java | 87 + .../feign/annotation/EnableTransferFeign.java | 33 + .../common/feign/filter/MdcFilter.java | 48 + .../feign/filter/TransferFeginFilter.java | 69 + .../common/feign/filter/TransferFilter.java | 63 + .../interceptor/FeignHeaderInterceptor.java | 48 + .../feign/interceptor/MdcInterceptor.java | 20 + .../feign/registrar/MdcFilterRegistrar.java | 37 + .../TransferFeginFilterRegistrar.java | 41 + .../registrar/TransferFilterRegistrar.java | 38 + .../sentinel/SentinelAutoConfiguration.java | 67 + .../common/sentinel/ext/OnnSentinelFeign.java | 129 + .../ext/OnnSentinelFilterConfiguration.java | 58 + .../ext/OnnSentinelInvocationHandler.java | 185 + .../handle/FeignExceptionHandler.java | 20 + .../sentinel/handle/OnnUrlBlockHandler.java | 50 + .../parser/OnnHeaderRequestOriginParser.java | 46 + .../ChushangFeignClientsRegistrar.java | 240 + .../main/resources/META-INF/spring.factories | 2 + chushang-common/chushang-common-job/pom.xml | 33 + .../job/core/config/XxlJobConfig.java | 133 + .../main/resources/META-INF/spring.factories | 2 + chushang-common/chushang-common-log/pom.xml | 34 + .../common/log/annotation/SysLog.java | 35 + .../common/log/aspect/SysLogAspect.java | 178 + .../log/controller/SysLogController.java | 45 + .../common/log/entity/SysLogEntity.java | 106 + .../common/log/entity/dto/ListLogDTO.java | 16 + .../common/log/enums/LogTypeEnum.java | 29 + .../common/log/mapper/SysLogMapper.java | 8 + .../common/log/service/SysLogService.java | 30 + .../log/service/impl/SysLogServiceImpl.java | 16 + .../main/resources/META-INF/spring.factories | 1 + .../main/resources/mapper/SysLogMapper.xml | 5 + chushang-common/chushang-common-mail/pom.xml | 36 + .../chushang/common/mail/utils/MailUtils.java | 43 + .../main/resources/META-INF/spring.factories | 2 + .../chushang-common-mongo/README.md | 2 + chushang-common/chushang-common-mongo/pom.xml | 25 + .../chushang/common/mongo/EnableMongo.java | 17 + .../common/mongo/annotation/MongoDel.java | 15 + .../common/mongo/config/MongoTransaction.java | 20 + .../chushang/common/mongo/entity/Garbage.java | 34 + .../mongo/listener/MongoEventListener.java | 50 + .../main/resources/META-INF/spring.factories | 3 + .../chushang-common-mybatis/pom.xml | 47 + .../mybatisplus/core/config/GlobalConfig.java | 204 + .../inner/PaginationInnerInterceptor.java | 476 ++ .../mybatis/MybatisAutoConfiguration.java | 102 + .../common/mybatis/base/BaseEntity.java | 78 + .../config/MybatisPlusMapperRefresh.java | 274 + .../config/MybatisPlusMetaObjectHandler.java | 62 + .../common/mybatis/enums/Operator.java | 55 + .../handler/MybatisExceptionHandler.java | 20 + .../resolver/SqlFilterArgumentResolver.java | 115 + .../common/mybatis/utils/PageClass.java | 34 + .../common/mybatis/utils/PageUtils.java | 118 + .../main/resources/META-INF/spring.factories | 3 + chushang-common/chushang-common-redis/pom.xml | 20 + .../redis/cache/PlusSpringCacheManager.java | 192 + .../redis/cache/utils/CacheUtils.java | 75 + .../redis/cache/utils/RedisUtils.java | 463 ++ .../redis/config/RedisConfiguration.java | 104 + .../config/properties/RedissonProperties.java | 100 + .../redis/hanlder/KeyPrefixHandler.java | 49 + .../main/resources/META-INF/spring.factories | 2 + .../src/main/resources/redisson-sentnel.yml | 28 + .../src/main/resources/redisson.yml | 38 + .../chushang-common-security/pom.xml | 32 + .../annotation/EnableCustomConfig.java | 24 + .../security/annotation/InnerAuth.java | 19 + .../chushang/security/annotation/Logical.java | 20 + .../security/annotation/RequiresLogin.java | 18 + .../annotation/RequiresPermissions.java | 27 + .../security/annotation/RequiresRoles.java | 27 + .../security/aspect/InnerAuthAspect.java | 51 + .../security/aspect/PreAuthorizeAspect.java | 90 + .../com/chushang/security/auth/AuthLogic.java | 259 + .../com/chushang/security/auth/AuthUtil.java | 71 + .../security/config/ApplicationConfig.java | 23 + .../security/config/WebMvcConfig.java | 63 + .../context/SecurityContextHolder.java | 99 + .../feign/FeignAutoConfiguration.java | 20 + .../feign/FeignRequestInterceptor.java | 83 + .../interceptor/HeaderInterceptor.java | 55 + .../security/service/TokenService.java | 198 + .../utils/NonWebRequestAttributes.java | 45 + .../security/utils/SecurityUtils.java | 107 + .../main/resources/META-INF/spring.factories | 5 + chushang-common/pom.xml | 31 + chushang-modules/.idea/compiler.xml | 31 + chushang-modules/.idea/encodings.xml | 57 + .../inspectionProfiles/Project_Default.xml | 7 + chushang-modules/.idea/jarRepositories.xml | 20 + chushang-modules/.idea/misc.xml | 12 + chushang-modules/.idea/uiDesigner.xml | 124 + chushang-modules/.idea/vcs.xml | 6 + chushang-modules/.idea/workspace.xml | 200 + .../chushang-module-auth/.gitignore | 64 + .../chushang-module-auth/auth-service/pom.xml | 60 + .../com/chushang/SanyiAuthApplication.java | 23 + .../auth/controller/UserController.java | 70 + .../chushang/auth/service/UserService.java | 110 + .../src/main/resources/bootstrap.yml | 32 + .../src/main/resources/logback-nacos.xml | 107 + chushang-modules/chushang-module-auth/pom.xml | 23 + .../chushang-module-gateway/.gitignore | 64 + .../chushang-module-gateway/pom.xml | 85 + .../com/chushang/SanyiGatewayApplication.java | 19 + .../gateway/config/CaptchaConfig.java | 85 + .../gateway/config/GatewayConfiguration.java | 55 + .../gateway/config/GatewayContext.java | 23 + .../gateway/config/KaptchaTextCreator.java | 76 + .../config/RateLimiterConfiguration.java | 45 + .../config/RouterFunctionConfiguration.java | 35 + .../gateway/config/pool/PoolUtils.java | 67 + .../config/properties/CaptchaProperties.java | 46 + .../properties/IgnoreWhiteProperties.java | 35 + .../config/properties/XssProperties.java | 49 + .../gateway/filter/ApiLoggingFilter.java | 64 + .../chushang/gateway/filter/AuthFilter.java | 169 + .../gateway/filter/BlackListUrlFilter.java | 69 + .../gateway/filter/CacheRequestFilter.java | 106 + .../filter/SanyiRequestGlobalFilter.java | 215 + .../filter/SanyiResponseGlobalFilter.java | 88 + .../gateway/filter/ValidateCodeFilter.java | 81 + .../chushang/gateway/filter/XssFilter.java | 122 + .../handler/GatewayExceptionHandler.java | 56 + .../handler/SentinelFallbackHandler.java | 41 + .../gateway/handler/ValidateCodeHandler.java | 42 + .../gateway/service/ValidateCodeService.java | 25 + .../service/impl/ValidateCodeServiceImpl.java | 112 + .../src/main/resources/bootstrap.yml | 35 + .../src/main/resources/logback-nacos.xml | 107 + .../src/test/java/RedisTest.java | 26 + .../chushang-module-oss/.gitignore | 64 + .../chushang-module-oss/oss-feign/pom.xml | 37 + .../com/chushang/oss/config/UploadConfig.java | 69 + .../chushang/oss/entity/FileSourceInfo.java | 41 + .../chushang-module-oss/oss-service/pom.xml | 40 + .../java/com/chushang/oss/OssApplication.java | 32 + .../chushang/oss/mapper/FileSourceMapper.java | 11 + .../com/chushang/oss/service/OssService.java | 81 + .../oss/service/impl/AliServiceImpl.java | 132 + .../oss/service/impl/FileSourceService.java | 20 + .../oss/service/impl/LocalServiceImpl.java | 11 + .../oss/service/impl/MinioServiceImpl.java | 61 + .../src/main/resources/bootstrap.yml | 31 + .../src/main/resources/logback-nacos.xml | 96 + chushang-modules/chushang-module-oss/pom.xml | 35 + .../chushang-module-system/.gitignore | 64 + .../chushang-module-system/pom.xml | 27 + .../system-feign/pom.xml | 31 + .../system/entity/bo/CancelUserRole.java | 17 + .../chushang/system/entity/bo/DataAuth.java | 28 + .../chushang/system/entity/bo/LoginBody.java | 19 + .../system/entity/bo/PasswordForm.java | 19 + .../chushang/system/entity/bo/RoleUser.java | 20 + .../system/entity/dto/GitlabUserDTO.java | 36 + .../system/entity/dto/ListDeptDTO.java | 22 + .../system/entity/dto/ListMenuDTO.java | 26 + .../system/entity/dto/ListRoleDTO.java | 17 + .../system/entity/dto/ListUserDTO.java | 20 + .../system/entity/enums/AuthTypeEnum.java | 41 + .../system/entity/enums/DataTypeEnum.java | 32 + .../system/entity/enums/LoginStatusEnum.java | 36 + .../system/entity/enums/MenuTypeEnum.java | 28 + .../system/entity/enums/PermTypeEnum.java | 40 + .../chushang/system/entity/po/SysDept.java | 75 + .../system/entity/po/SysDictData.java | 67 + .../system/entity/po/SysLoginInfo.java | 45 + .../chushang/system/entity/po/SysMenu.java | 115 + .../chushang/system/entity/po/SysRole.java | 123 + .../system/entity/po/SysRoleDept.java | 41 + .../system/entity/po/SysRoleMenu.java | 45 + .../chushang/system/entity/po/SysUser.java | 113 + .../system/entity/po/SysUserData.java | 37 + .../system/entity/po/SysUserPost.java | 47 + .../system/entity/po/SysUserRole.java | 47 + .../chushang/system/entity/vo/LoginUser.java | 69 + .../com/chushang/system/entity/vo/MetaVo.java | 102 + .../chushang/system/entity/vo/RouterVo.java | 144 + .../chushang/system/entity/vo/TreeSelect.java | 78 + .../system/feign/RemoteAuthDataService.java | 25 + .../system/feign/RemoteLoginInfoService.java | 30 + .../system/feign/RemoteUserService.java | 33 + .../main/resources/META-INF/spring.factories | 1 + .../system-service/pom.xml | 40 + .../chushang/system/SystemApplication.java | 22 + .../chushang/system/annotation/DataScope.java | 21 + .../system/aspect/DataScopeAspect.java | 181 + .../system/controller/DeptController.java | 160 + .../system/controller/DictController.java | 30 + .../system/controller/MenuController.java | 161 + .../system/controller/RoleController.java | 211 + .../controller/SysLoginInfoController.java | 27 + .../controller/SysUserDataController.java | 77 + .../system/controller/UserController.java | 321 ++ .../chushang/system/mapper/SysDeptMapper.java | 26 + .../system/mapper/SysDictDataMapper.java | 10 + .../system/mapper/SysLoginInfoMapper.java | 7 + .../chushang/system/mapper/SysMenuMapper.java | 36 + .../system/mapper/SysRoleDeptMapper.java | 16 + .../chushang/system/mapper/SysRoleMapper.java | 30 + .../system/mapper/SysRoleMenuMapper.java | 16 + .../system/mapper/SysUserDataMapper.java | 10 + .../chushang/system/mapper/SysUserMapper.java | 39 + .../system/mapper/SysUserPostMapper.java | 10 + .../system/mapper/SysUserRoleMapper.java | 16 + .../system/service/ISysDeptService.java | 122 + .../system/service/ISysMenuService.java | 63 + .../system/service/ISysPermissionService.java | 14 + .../system/service/ISysRoleDeptService.java | 49 + .../system/service/ISysRoleMenuService.java | 52 + .../system/service/ISysRoleService.java | 78 + .../system/service/ISysUserDataService.java | 61 + .../system/service/ISysUserPostService.java | 33 + .../system/service/ISysUserRoleService.java | 66 + .../system/service/ISysUserService.java | 80 + .../system/service/RemoteService.java | 62 + .../system/service/SysDictDataService.java | 13 + .../system/service/SysLoginInfoService.java | 13 + .../service/impl/ISysUserDataServiceImpl.java | 55 + .../service/impl/SysDeptServiceImpl.java | 147 + .../service/impl/SysDictDataServiceImpl.java | 51 + .../service/impl/SysLoginInfoServiceImpl.java | 14 + .../service/impl/SysMenuServiceImpl.java | 356 ++ .../impl/SysPermissionServiceImpl.java | 73 + .../service/impl/SysRoleDeptServiceImpl.java | 22 + .../service/impl/SysRoleMenuServiceImpl.java | 22 + .../service/impl/SysRoleServiceImpl.java | 180 + .../service/impl/SysUserRoleServiceImpl.java | 22 + .../service/impl/SysUserServiceImpl.java | 227 + .../src/main/resources/application.yml | 70 + .../src/main/resources/bootstrap.yml | 40 + .../src/main/resources/logback-nacos.xml | 96 + .../main/resources/mapper/SysDeptMapper.xml | 65 + .../main/resources/mapper/SysMenuMapper.xml | 181 + .../main/resources/mapper/SysRoleMapper.xml | 89 + .../main/resources/mapper/SysUserMapper.xml | 121 + .../src/test/java/DemoTest.java | 100 + .../chushang-module-system/system.md | 17 + chushang-modules/pom.xml | 21 + chushang-visual/chushang-admin/.gitignore | 64 + chushang-visual/chushang-admin/pom.xml | 68 + .../chushang/admin/SanyiAdminApplication.java | 18 + .../admin/config/SecuritySecureConfig.java | 61 + .../src/main/resources/bootstrap.yml | 38 + .../src/main/resources/logback-nacos.xml | 125 + chushang-visual/chushang-sentinel/.gitignore | 64 + chushang-visual/chushang-sentinel/pom.xml | 106 + .../dashboard/SanyiSentinelApplication.java | 39 + .../sentinel/dashboard/auth/AuthAction.java | 44 + .../sentinel/dashboard/auth/AuthService.java | 112 + .../auth/AuthorizationInterceptor.java | 73 + .../dashboard/auth/FakeAuthServiceImpl.java | 67 + .../auth/LoginAuthenticationFilter.java | 133 + .../auth/SimpleWebAuthServiceImpl.java | 82 + .../client/CommandFailedException.java | 35 + .../client/CommandNotFoundException.java | 36 + .../dashboard/client/SentinelApiClient.java | 884 ++++ .../dashboard/config/DashboardConfig.java | 149 + .../sentinel/dashboard/config/WebConfig.java | 115 + .../dashboard/controller/AppController.java | 81 + .../dashboard/controller/AuthController.java | 94 + .../controller/AuthorityRuleController.java | 186 + .../controller/DegradeController.java | 219 + .../dashboard/controller/DemoController.java | 141 + .../controller/FlowControllerV1.java | 267 + .../controller/MachineRegistryController.java | 80 + .../controller/MetricController.java | 158 + .../controller/ParamFlowRuleController.java | 271 + .../controller/ResourceController.java | 91 + .../controller/SystemController.java | 257 + .../controller/VersionController.java | 50 + .../cluster/ClusterAssignController.java | 102 + .../cluster/ClusterConfigController.java | 241 + .../gateway/GatewayApiController.java | 267 + .../gateway/GatewayFlowRuleController.java | 445 ++ .../v2/AuthorityRuleControllerV2.java | 186 + .../controller/v2/DegradeControllerV2.java | 220 + .../controller/v2/FlowControllerV2.java | 222 + .../controller/v2/GatewayApiControllerV2.java | 256 + .../v2/GatewayFlowRuleControllerV2.java | 439 ++ .../v2/ParamFlowRuleControllerV2.java | 267 + .../datasource/entity/ApplicationEntity.java | 108 + .../datasource/entity/MachineEntity.java | 127 + .../datasource/entity/MetricEntity.java | 241 + .../entity/MetricPositionEntity.java | 121 + .../datasource/entity/SentinelVersion.java | 136 + .../entity/gateway/ApiDefinitionEntity.java | 208 + .../gateway/ApiPredicateItemEntity.java | 80 + .../entity/gateway/GatewayFlowRuleEntity.java | 352 ++ .../gateway/GatewayParamFlowItemEntity.java | 94 + .../entity/rule/AbstractRuleEntity.java | 115 + .../entity/rule/AuthorityRuleEntity.java | 63 + .../entity/rule/DegradeRuleEntity.java | 213 + .../entity/rule/FlowRuleEntity.java | 263 + .../entity/rule/ParamFlowRuleEntity.java | 121 + .../datasource/entity/rule/RuleEntity.java | 41 + .../entity/rule/SystemRuleEntity.java | 167 + .../sentinel/dashboard/discovery/AppInfo.java | 125 + .../dashboard/discovery/AppManagement.java | 69 + .../dashboard/discovery/MachineDiscovery.java | 50 + .../dashboard/discovery/MachineInfo.java | 186 + .../discovery/SimpleMachineDiscovery.java | 77 + .../dashboard/domain/ResourceTreeNode.java | 257 + .../csp/sentinel/dashboard/domain/Result.java | 97 + .../cluster/ClusterAppAssignResultVO.java | 65 + .../cluster/ClusterAppFullAssignRequest.java | 56 + .../ClusterAppSingleServerAssignRequest.java | 56 + .../domain/cluster/ClusterClientInfoVO.java | 74 + .../domain/cluster/ClusterGroupEntity.java | 88 + .../domain/cluster/ClusterStateSingleVO.java | 63 + .../cluster/ConnectionDescriptorVO.java | 51 + .../domain/cluster/ConnectionGroupVO.java | 65 + .../cluster/config/ClusterClientConfig.java | 74 + .../cluster/config/ServerFlowConfig.java | 110 + .../cluster/config/ServerTransportConfig.java | 64 + .../cluster/request/ClusterAppAssignMap.java | 110 + .../request/ClusterClientModifyRequest.java | 85 + .../cluster/request/ClusterModifyRequest.java | 32 + .../request/ClusterServerModifyRequest.java | 117 + .../state/AppClusterClientStateWrapVO.java | 77 + .../state/AppClusterServerStateWrapVO.java | 99 + .../cluster/state/ClusterClientStateVO.java | 45 + .../cluster/state/ClusterRequestLimitVO.java | 63 + .../cluster/state/ClusterServerStateVO.java | 126 + .../state/ClusterStateSimpleEntity.java | 74 + .../state/ClusterUniversalStatePairVO.java | 72 + .../state/ClusterUniversalStateVO.java | 63 + .../dashboard/domain/vo/MachineInfoVo.java | 130 + .../dashboard/domain/vo/MetricVo.java | 220 + .../dashboard/domain/vo/ResourceVo.java | 249 + .../domain/vo/gateway/api/AddApiReqVo.java | 78 + .../vo/gateway/api/ApiPredicateItemVo.java | 46 + .../domain/vo/gateway/api/UpdateApiReqVo.java | 58 + .../vo/gateway/rule/AddFlowRuleReqVo.java | 156 + .../gateway/rule/GatewayParamFlowItemVo.java | 66 + .../vo/gateway/rule/UpdateFlowRuleReqVo.java | 126 + .../dashboard/metric/MetricFetcher.java | 382 ++ .../gateway/InMemApiDefinitionStore.java | 40 + .../gateway/InMemGatewayFlowRuleStore.java | 40 + .../metric/InMemoryMetricsRepository.java | 177 + .../repository/metric/MetricsRepository.java | 58 + .../rule/InMemAuthorityRuleStore.java | 39 + .../rule/InMemDegradeRuleStore.java | 36 + .../repository/rule/InMemFlowRuleStore.java | 53 + .../rule/InMemParamFlowRuleStore.java | 51 + .../repository/rule/InMemSystemRuleStore.java | 36 + .../rule/InMemoryRuleRepositoryAdapter.java | 131 + .../repository/rule/RuleRepository.java | 80 + .../dashboard/rule/DynamicRuleProvider.java | 26 + .../dashboard/rule/DynamicRulePublisher.java | 32 + .../dashboard/rule/FlowRuleApiProvider.java | 60 + .../dashboard/rule/FlowRuleApiPublisher.java | 61 + .../rule/nacos/AuthRuleNacosProvider.java | 50 + .../rule/nacos/AuthRuleNacosPublisher.java | 49 + .../rule/nacos/DegradeRuleNacosProvider.java | 50 + .../rule/nacos/DegradeRuleNacosPublisher.java | 49 + .../rule/nacos/FlowRuleNacosProvider.java | 50 + .../rule/nacos/FlowRuleNacosPublisher.java | 49 + .../rule/nacos/GatewayApiNacosProvider.java | 50 + .../rule/nacos/GatewayApiNacosPublisher.java | 33 + .../nacos/GatewayFlowRuleNacosProvider.java | 50 + .../nacos/GatewayFlowRuleNacosPublisher.java | 50 + .../dashboard/rule/nacos/NacosConfig.java | 125 + .../dashboard/rule/nacos/NacosConfigUtil.java | 48 + .../nacos/ParamFlowRuleNacosProvider.java | 50 + .../nacos/ParamFlowRuleNacosPublisher.java | 50 + .../service/ClusterAssignService.java | 56 + .../service/ClusterAssignServiceImpl.java | 241 + .../service/ClusterConfigService.java | 172 + .../sentinel/dashboard/util/AsyncUtils.java | 68 + .../dashboard/util/ClusterEntityUtils.java | 142 + .../sentinel/dashboard/util/MachineUtils.java | 63 + .../sentinel/dashboard/util/VersionUtils.java | 95 + .../dashboard/util/pool/PoolUtils.java | 67 + .../src/main/resources/bootstrap.yml | 30 + .../src/main/resources/logback-nacos.xml | 121 + .../main/webapp/resources/.idea/.gitignore | 5 + .../inspectionProfiles/Project_Default.xml | 6 + .../resources/.idea/jsLinters/jshint.xml | 16 + .../main/webapp/resources/.idea/modules.xml | 8 + .../src/main/webapp/resources/.idea/vcs.xml | 6 + .../src/main/webapp/resources/.jshintrc | 67 + .../src/main/webapp/resources/README.md | 32 + .../src/main/webapp/resources/README_zh.md | 32 + .../main/webapp/resources/app/scripts/app.js | 365 ++ .../app/scripts/controllers/authority.js | 227 + .../controllers/cluster_app_assign_manage.js | 283 ++ .../controllers/cluster_app_server_list.js | 570 +++ .../controllers/cluster_app_server_manage.js | 283 ++ .../controllers/cluster_app_server_monitor.js | 97 + .../cluster_app_token_client_list.js | 121 + .../app/scripts/controllers/cluster_single.js | 262 + .../app/scripts/controllers/degrade.js | 204 + .../app/scripts/controllers/flow_v1.js | 220 + .../app/scripts/controllers/flow_v2.js | 221 + .../app/scripts/controllers/gateway/api.js | 245 + .../app/scripts/controllers/gateway/flow.js | 251 + .../scripts/controllers/gateway/identity.js | 299 ++ .../resources/app/scripts/controllers/home.js | 11 + .../app/scripts/controllers/identity.js | 478 ++ .../app/scripts/controllers/login.js | 33 + .../app/scripts/controllers/machine.js | 65 + .../resources/app/scripts/controllers/main.js | 10 + .../app/scripts/controllers/metric.js | 268 + .../app/scripts/controllers/param_flow.js | 328 ++ .../app/scripts/controllers/system.js | 239 + .../app/scripts/directives/header/header.html | 15 + .../app/scripts/directives/header/header.js | 61 + .../sidebar-search/sidebar-search.html | 10 + .../sidebar/sidebar-search/sidebar-search.js | 20 + .../scripts/directives/sidebar/sidebar.html | 91 + .../app/scripts/directives/sidebar/sidebar.js | 71 + .../resources/app/scripts/filters/filters.js | 17 + .../resources/app/scripts/libs/treeTable.js | 292 ++ .../app/scripts/services/appservice.js | 12 + .../app/scripts/services/auth_service.js | 25 + .../app/scripts/services/authority_service.js | 56 + .../scripts/services/cluster_state_service.js | 73 + .../app/scripts/services/degrade_service.js | 97 + .../app/scripts/services/flow_service_v1.js | 119 + .../app/scripts/services/flow_service_v2.js | 85 + .../scripts/services/gateway/api_service.js | 73 + .../scripts/services/gateway/flow_service.js | 76 + .../app/scripts/services/identityservice.js | 30 + .../app/scripts/services/machineservice.js | 25 + .../app/scripts/services/metricservice.js | 36 + .../scripts/services/param_flow_service.js | 104 + .../app/scripts/services/systemservice.js | 77 + .../app/scripts/services/version_service.js | 10 + .../main/webapp/resources/app/styles/main.css | 1756 +++++++ .../main/webapp/resources/app/styles/page.css | 399 ++ .../webapp/resources/app/styles/timeline.css | 180 + .../webapp/resources/app/views/authority.html | 85 + .../resources/app/views/cluster/client.html | 30 + .../resources/app/views/cluster/server.html | 29 + .../app/views/cluster_app_assign_manage.html | 118 + .../app/views/cluster_app_client_list.html | 73 + .../app/views/cluster_app_server_list.html | 96 + .../views/cluster_app_server_overview.html | 88 + .../app/views/cluster_single_config.html | 95 + .../resources/app/views/dashboard/home.html | 13 + .../resources/app/views/dashboard/main.html | 10 + .../webapp/resources/app/views/degrade.html | 98 + .../views/dialog/authority-rule-dialog.html | 46 + .../cluster/cluster-client-config-dialog.html | 40 + .../cluster/cluster-server-assign-dialog.html | 139 + ...uster-server-connection-detail-dialog.html | 37 + .../app/views/dialog/confirm-dialog.html | 20 + .../app/views/dialog/degrade-rule-dialog.html | 83 + .../app/views/dialog/flow-rule-dialog.html | 148 + .../app/views/dialog/gateway/api-dialog.html | 49 + .../dialog/gateway/flow-rule-dialog.html | 172 + .../views/dialog/param-flow-rule-dialog.html | 166 + .../app/views/dialog/system-rule-dialog.html | 58 + .../webapp/resources/app/views/flow_v1.html | 117 + .../webapp/resources/app/views/flow_v2.html | 113 + .../resources/app/views/gateway/api.html | 87 + .../resources/app/views/gateway/flow.html | 94 + .../resources/app/views/gateway/identity.html | 98 + .../webapp/resources/app/views/identity.html | 110 + .../webapp/resources/app/views/login.html | 34 + .../webapp/resources/app/views/machine.html | 76 + .../webapp/resources/app/views/metric.html | 120 + .../resources/app/views/pagination.tpl.html | 18 + .../resources/app/views/param_flow.html | 118 + .../webapp/resources/app/views/system.html | 92 + .../resources/assets/img/sentinel-logo.png | 0 .../main/webapp/resources/dist/css/app.css | 5 + .../src/main/webapp/resources/dist/js/app.js | 1 + .../webapp/resources/dist/js/app.vendor.js | 1 + .../src/main/webapp/resources/gulpfile.js | 134 + .../src/main/webapp/resources/index.htm | 28 + .../src/main/webapp/resources/index_dev.htm | 28 + .../resources/lib/css/bootstrap.min.css | 7 + .../resources/lib/css/font-awesome.min.css | 4 + .../lib/fonts/fontawesome-webfont.ttf | Bin 0 -> 165548 bytes .../lib/fonts/fontawesome-webfont.woff | 0 .../lib/fonts/fontawesome-webfont.woff2 | 0 .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 41236 bytes .../fonts/glyphicons-halflings-regular.woff | 0 .../webapp/resources/lib/js/angular.min.js | 295 ++ .../webapp/resources/lib/js/bootstrap.min.js | 7 + .../main/webapp/resources/lib/js/g2.min.js | 1 + .../webapp/resources/lib/js/jquery.min.js | 5 + .../main/webapp/resources/license-stat.csv | 26 + .../main/webapp/resources/package-lock.json | 4498 +++++++++++++++++ .../src/main/webapp/resources/package.json | 55 + .../chushang-sentinel/src/test/java/Test.java | 22 + chushang-visual/pom.xml | 20 + nginx.md | 3 + pom.xml | 614 +++ redisson-config.yml | 112 + service-docker-compose.yml | 92 + 599 files changed, 59876 insertions(+) create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 chushang-centrale/chushang-canal/.gitignore create mode 100644 chushang-centrale/chushang-canal/pom.xml create mode 100644 chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/SanyiCanalApplication.java create mode 100644 chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/sync/SystemProjectConfigListen.java create mode 100644 chushang-centrale/chushang-canal/src/main/resources/bootstrap.yml create mode 100644 chushang-centrale/chushang-canal/src/main/resources/logback-nacos.xml create mode 100644 chushang-centrale/pom.xml create mode 100644 chushang-common/.gitignore create mode 100644 chushang-common/chushang-common-bom/pom.xml create mode 100644 chushang-common/chushang-common-canal/pom.xml create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/CanalEventListener.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/DeleteListenPoint.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/InsertListenPoint.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/ListenPoint.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/UpdateListenPoint.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/AbstractCanalClient.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/CanalClient.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/ListenerPoint.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/SimpleCanalClient.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractBasicMessageTransponder.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractMessageTransponder.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultMessageTransponder.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultTransponderFactory.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponder.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponders.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/TransponderFactory.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalClientAutoConfiguration.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalConfig.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/event/CanalEventListener.java create mode 100644 chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/util/BeanUtil.java create mode 100644 chushang-common/chushang-common-canal/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-core/pom.xml create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/cache/CacheNameSpase.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/config/JacksonConfiguration.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CacheConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CharConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CommonConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/Constants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/RegularConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/SecurityConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/ServiceNameConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/TokenConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UnityLanguageConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UserConstants.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/enums/AppStartType.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/CheckedException.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/InnerAuthException.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ResultException.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ServiceException.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotLoginException.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotPermissionException.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotRoleException.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/enums/ExceptionEnum.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/utils/AssertUtil.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/handler/GlobalExceptionHandler.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JacksonUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JavaTimeModule.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/CharsetKit.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/Convert.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/StrFormatter.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/ToolUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ClassUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/CommonParam.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ConcurrencyUtilTest.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/DateUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/FileUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/HexByte.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IPUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IdUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/JwtUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/MD5Util.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/PackageUtil.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/RequestUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ServletUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/SpringUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/StringUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/AjaxResult.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/EnumUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/Result.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/TokenUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/ValidList.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/WebUtils.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/EscapeUtil.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/HTMLFilter.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/SQLFilter.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/Xss.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssFilter.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssHttpServletRequestWrapper.java create mode 100644 chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssValidator.java create mode 100644 chushang-common/chushang-common-core/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-easy-es/pom.xml create mode 100644 chushang-common/chushang-common-easy-es/src/main/java/com/chushang/common/ee/EasyEsConfig.java create mode 100644 chushang-common/chushang-common-easy-es/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-excel/pom.xml create mode 100644 chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/DataListener.java create mode 100644 chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/ImportListener.java create mode 100644 chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/service/ExcelImporter.java create mode 100644 chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/utils/ExcelUtils.java create mode 100644 chushang-common/chushang-common-excel/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-feign/pom.xml create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableOnnFeignClients.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableTransferFeign.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/MdcFilter.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFeginFilter.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFilter.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/FeignHeaderInterceptor.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/MdcInterceptor.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/MdcFilterRegistrar.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFeginFilterRegistrar.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFilterRegistrar.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/SentinelAutoConfiguration.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFeign.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFilterConfiguration.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelInvocationHandler.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/FeignExceptionHandler.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/OnnUrlBlockHandler.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/parser/OnnHeaderRequestOriginParser.java create mode 100644 chushang-common/chushang-common-feign/src/main/java/org/springframework/cloud/openfeign/ChushangFeignClientsRegistrar.java create mode 100644 chushang-common/chushang-common-feign/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-job/pom.xml create mode 100644 chushang-common/chushang-common-job/src/main/java/com/chushang/job/core/config/XxlJobConfig.java create mode 100644 chushang-common/chushang-common-job/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-log/pom.xml create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/annotation/SysLog.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/aspect/SysLogAspect.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/controller/SysLogController.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/SysLogEntity.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/dto/ListLogDTO.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/enums/LogTypeEnum.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/mapper/SysLogMapper.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/SysLogService.java create mode 100644 chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/impl/SysLogServiceImpl.java create mode 100644 chushang-common/chushang-common-log/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-log/src/main/resources/mapper/SysLogMapper.xml create mode 100644 chushang-common/chushang-common-mail/pom.xml create mode 100644 chushang-common/chushang-common-mail/src/main/java/com/chushang/common/mail/utils/MailUtils.java create mode 100644 chushang-common/chushang-common-mail/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-mongo/README.md create mode 100644 chushang-common/chushang-common-mongo/pom.xml create mode 100644 chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/EnableMongo.java create mode 100644 chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/annotation/MongoDel.java create mode 100644 chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/config/MongoTransaction.java create mode 100644 chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/entity/Garbage.java create mode 100644 chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/listener/MongoEventListener.java create mode 100644 chushang-common/chushang-common-mongo/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-mybatis/pom.xml create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/core/config/GlobalConfig.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/extension/plugins/inner/PaginationInnerInterceptor.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/MybatisAutoConfiguration.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/base/BaseEntity.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMapperRefresh.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMetaObjectHandler.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/enums/Operator.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/handler/MybatisExceptionHandler.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/resolver/SqlFilterArgumentResolver.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageClass.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageUtils.java create mode 100644 chushang-common/chushang-common-mybatis/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-redis/pom.xml create mode 100644 chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/PlusSpringCacheManager.java create mode 100644 chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/CacheUtils.java create mode 100644 chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/RedisUtils.java create mode 100644 chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/RedisConfiguration.java create mode 100644 chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/properties/RedissonProperties.java create mode 100644 chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/hanlder/KeyPrefixHandler.java create mode 100644 chushang-common/chushang-common-redis/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/chushang-common-redis/src/main/resources/redisson-sentnel.yml create mode 100644 chushang-common/chushang-common-redis/src/main/resources/redisson.yml create mode 100644 chushang-common/chushang-common-security/pom.xml create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/EnableCustomConfig.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/InnerAuth.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/Logical.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresLogin.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresPermissions.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresRoles.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/InnerAuthAspect.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/PreAuthorizeAspect.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthLogic.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthUtil.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/ApplicationConfig.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/WebMvcConfig.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/context/SecurityContextHolder.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignAutoConfiguration.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignRequestInterceptor.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/interceptor/HeaderInterceptor.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/service/TokenService.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/NonWebRequestAttributes.java create mode 100644 chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/SecurityUtils.java create mode 100644 chushang-common/chushang-common-security/src/main/resources/META-INF/spring.factories create mode 100644 chushang-common/pom.xml create mode 100644 chushang-modules/.idea/compiler.xml create mode 100644 chushang-modules/.idea/encodings.xml create mode 100644 chushang-modules/.idea/inspectionProfiles/Project_Default.xml create mode 100644 chushang-modules/.idea/jarRepositories.xml create mode 100644 chushang-modules/.idea/misc.xml create mode 100644 chushang-modules/.idea/uiDesigner.xml create mode 100644 chushang-modules/.idea/vcs.xml create mode 100644 chushang-modules/.idea/workspace.xml create mode 100644 chushang-modules/chushang-module-auth/.gitignore create mode 100644 chushang-modules/chushang-module-auth/auth-service/pom.xml create mode 100644 chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/SanyiAuthApplication.java create mode 100644 chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/controller/UserController.java create mode 100644 chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/service/UserService.java create mode 100644 chushang-modules/chushang-module-auth/auth-service/src/main/resources/bootstrap.yml create mode 100644 chushang-modules/chushang-module-auth/auth-service/src/main/resources/logback-nacos.xml create mode 100644 chushang-modules/chushang-module-auth/pom.xml create mode 100644 chushang-modules/chushang-module-gateway/.gitignore create mode 100644 chushang-modules/chushang-module-gateway/pom.xml create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/SanyiGatewayApplication.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/CaptchaConfig.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayConfiguration.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayContext.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/KaptchaTextCreator.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RateLimiterConfiguration.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RouterFunctionConfiguration.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/pool/PoolUtils.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/CaptchaProperties.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/IgnoreWhiteProperties.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/XssProperties.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ApiLoggingFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/AuthFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/BlackListUrlFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/CacheRequestFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiRequestGlobalFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiResponseGlobalFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ValidateCodeFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/XssFilter.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/GatewayExceptionHandler.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/SentinelFallbackHandler.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/ValidateCodeHandler.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/ValidateCodeService.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/impl/ValidateCodeServiceImpl.java create mode 100644 chushang-modules/chushang-module-gateway/src/main/resources/bootstrap.yml create mode 100644 chushang-modules/chushang-module-gateway/src/main/resources/logback-nacos.xml create mode 100644 chushang-modules/chushang-module-gateway/src/test/java/RedisTest.java create mode 100644 chushang-modules/chushang-module-oss/.gitignore create mode 100644 chushang-modules/chushang-module-oss/oss-feign/pom.xml create mode 100644 chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/config/UploadConfig.java create mode 100644 chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/entity/FileSourceInfo.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/pom.xml create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/OssApplication.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/mapper/FileSourceMapper.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/OssService.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/AliServiceImpl.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/FileSourceService.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/LocalServiceImpl.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/MinioServiceImpl.java create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/resources/bootstrap.yml create mode 100644 chushang-modules/chushang-module-oss/oss-service/src/main/resources/logback-nacos.xml create mode 100644 chushang-modules/chushang-module-oss/pom.xml create mode 100644 chushang-modules/chushang-module-system/.gitignore create mode 100644 chushang-modules/chushang-module-system/pom.xml create mode 100644 chushang-modules/chushang-module-system/system-feign/pom.xml create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/CancelUserRole.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/DataAuth.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/LoginBody.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/PasswordForm.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/RoleUser.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/GitlabUserDTO.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListDeptDTO.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListMenuDTO.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListRoleDTO.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListUserDTO.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/AuthTypeEnum.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/DataTypeEnum.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/LoginStatusEnum.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/MenuTypeEnum.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/PermTypeEnum.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDept.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDictData.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysLoginInfo.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysMenu.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRole.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleDept.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleMenu.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUser.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserData.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserPost.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserRole.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/LoginUser.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/MetaVo.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/RouterVo.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/TreeSelect.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteAuthDataService.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteLoginInfoService.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteUserService.java create mode 100644 chushang-modules/chushang-module-system/system-feign/src/main/resources/META-INF/spring.factories create mode 100644 chushang-modules/chushang-module-system/system-service/pom.xml create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/SystemApplication.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/annotation/DataScope.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/aspect/DataScopeAspect.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DeptController.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DictController.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/MenuController.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/RoleController.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysLoginInfoController.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysUserDataController.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/UserController.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDeptMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDictDataMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysLoginInfoMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysMenuMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleDeptMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMenuMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserDataMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserPostMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserRoleMapper.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysDeptService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysMenuService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysPermissionService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleDeptService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleMenuService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserDataService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserPostService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserRoleService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/RemoteService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysDictDataService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysLoginInfoService.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/ISysUserDataServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDeptServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDictDataServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysLoginInfoServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysMenuServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysPermissionServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleDeptServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleMenuServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserRoleServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserServiceImpl.java create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/resources/application.yml create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/resources/bootstrap.yml create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/resources/logback-nacos.xml create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysDeptMapper.xml create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysMenuMapper.xml create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysRoleMapper.xml create mode 100644 chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysUserMapper.xml create mode 100644 chushang-modules/chushang-module-system/system-service/src/test/java/DemoTest.java create mode 100644 chushang-modules/chushang-module-system/system.md create mode 100644 chushang-modules/pom.xml create mode 100644 chushang-visual/chushang-admin/.gitignore create mode 100644 chushang-visual/chushang-admin/pom.xml create mode 100644 chushang-visual/chushang-admin/src/main/java/com/chushang/admin/SanyiAdminApplication.java create mode 100644 chushang-visual/chushang-admin/src/main/java/com/chushang/admin/config/SecuritySecureConfig.java create mode 100644 chushang-visual/chushang-admin/src/main/resources/bootstrap.yml create mode 100644 chushang-visual/chushang-admin/src/main/resources/logback-nacos.xml create mode 100644 chushang-visual/chushang-sentinel/.gitignore create mode 100644 chushang-visual/chushang-sentinel/pom.xml create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/SanyiSentinelApplication.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthAction.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthService.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthorizationInterceptor.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/FakeAuthServiceImpl.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/LoginAuthenticationFilter.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandFailedException.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandNotFoundException.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/DashboardConfig.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DemoController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MetricController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ResourceController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/VersionController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterAssignController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterConfigController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/AuthorityRuleControllerV2.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/DegradeControllerV2.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayApiControllerV2.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayFlowRuleControllerV2.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/ParamFlowRuleControllerV2.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MachineEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/SentinelVersion.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/DegradeRuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/FlowRuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/RuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppManagement.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineDiscovery.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/Result.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppAssignResultVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppFullAssignRequest.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppSingleServerAssignRequest.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterClientInfoVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterGroupEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterStateSingleVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionDescriptorVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionGroupVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ClusterClientConfig.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerFlowConfig.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerTransportConfig.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterAppAssignMap.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterClientModifyRequest.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterModifyRequest.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterServerModifyRequest.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterClientStateWrapVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterServerStateWrapVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterClientStateVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterRequestLimitVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterServerStateVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterStateSimpleEntity.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStatePairVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStateVO.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MachineInfoVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MetricVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/ResourceVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/metric/MetricFetcher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/InMemoryMetricsRepository.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/MetricsRepository.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemAuthorityRuleStore.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemDegradeRuleStore.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemFlowRuleStore.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemParamFlowRuleStore.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemSystemRuleStore.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/RuleRepository.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRuleProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRulePublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiPublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosPublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosPublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosPublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosPublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosPublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfig.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigUtil.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosProvider.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosPublisher.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignService.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignServiceImpl.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterConfigService.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/AsyncUtils.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/ClusterEntityUtils.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/MachineUtils.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/VersionUtils.java create mode 100644 chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/pool/PoolUtils.java create mode 100644 chushang-visual/chushang-sentinel/src/main/resources/bootstrap.yml create mode 100644 chushang-visual/chushang-sentinel/src/main/resources/logback-nacos.xml create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/.gitignore create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/inspectionProfiles/Project_Default.xml create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/jsLinters/jshint.xml create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/modules.xml create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/vcs.xml create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/.jshintrc create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/README.md create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/README_zh.md create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/app.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/authority.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_assign_manage.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_list.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_manage.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_monitor.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_token_client_list.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_single.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/degrade.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v1.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v2.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/api.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/home.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/identity.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/login.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/machine.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/main.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/metric.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/param_flow.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/system.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/filters/filters.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/libs/treeTable.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/appservice.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/auth_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/authority_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/cluster_state_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/degrade_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v1.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v2.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/api_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/flow_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/identityservice.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/machineservice.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/metricservice.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/param_flow_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/systemservice.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/version_service.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/main.css create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/page.css create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/timeline.css create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/authority.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/client.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/server.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_assign_manage.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_client_list.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_list.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_overview.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_single_config.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/home.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/main.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/degrade.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/authority-rule-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-client-config-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-assign-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-connection-detail-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/confirm-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v1.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v2.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/api.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/flow.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/identity.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/identity.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/login.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/machine.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/metric.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/pagination.tpl.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/param_flow.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/system.html create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/assets/img/sentinel-logo.png create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/css/app.css create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/js/app.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/js/app.vendor.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/gulpfile.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/index.htm create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/index_dev.htm create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/bootstrap.min.css create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/font-awesome.min.css create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/fonts/fontawesome-webfont.ttf create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/fonts/fontawesome-webfont.woff create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/fonts/fontawesome-webfont.woff2 create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/fonts/glyphicons-halflings-regular.ttf create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/fonts/glyphicons-halflings-regular.woff create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/angular.min.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/bootstrap.min.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/g2.min.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/jquery.min.js create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/license-stat.csv create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/package-lock.json create mode 100644 chushang-visual/chushang-sentinel/src/main/webapp/resources/package.json create mode 100644 chushang-visual/chushang-sentinel/src/test/java/Test.java create mode 100644 chushang-visual/pom.xml create mode 100644 nginx.md create mode 100644 pom.xml create mode 100644 redisson-config.yml create mode 100644 service-docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..2b0cc2f --- /dev/null +++ b/Readme.md @@ -0,0 +1,204 @@ +### 模块说明 +```txt +sanyicloud --> +├── centrale -- 中间件 + ├── canal -- canal +├── common -- 系统公共模块 + ├── common-canal -- 添加此依赖后就可以使用canal + ├── common-core -- 公共工具类核心包 + ├── common-easy-es -- 针对easy-es 的相关配置依赖项 --> 未使用es + ├── common-excel -- 公共工具之 表格导入导出 + ├── common-feign -- feign 扩展封装 + ├── common-job -- 定时任务扩展封装 + ├── common-log -- 日志扩展封装 + ├── common-mail -- 邮件 扩展封装 + ├── common-mongo -- mongoDB 扩展封装 + ├── common-mybatis -- orm (Object Relational Mapping) 框架, mybatis-plus 扩展封装 + ├── common-oss -- 文件存储 扩展封装 + ├── common-security -- 全局登录注册 +├── -modules bi 平台相关 + ├── -module-auth 部署于阿里云测试和华为正式 --> 使用华为docker redis + ├── -module-common bi平台相关公共类, 主要为 日志, xxl-job, security 等 + ├── -module-fast bi平台收入与支出拉取等 --> 使用华为云 docker mysql, redis + ├── -module-gateway bi平台网关 --> 使用华为云 docker redis + ├── -module-job bi平台定时任务 --> 使用华为云 docker mysql, redis + ├── -module-manager bi平台项目管理 --> 使用阿里云 mysql 华为云 redis + ├── -module-review bi平台评论管理 --> 使用阿里云 mysql redis + ├── -module-system-v2 bi平台系统管理 --> 使用阿里云 mysql redis + ├── -module-website bi平台招聘以及叁一官网 --> 使用华为云 docker mysql redis + └── -tp-data 拉取tp元数据 --> 使用阿里云 mongo mysql +└── visual 可视化相关 + └── -sentinel -- 流量高可用 [5003] , 并进行了 nacos 持久化配置, 默认的 group 为 SENTINEL_GROUP +``` + +hadoop 安装教程 https://www.cnblogs.com/jhno1/p/15218656.html +mongo 单节点开启事务 https://www.jianshu.com/p/5a03b956ce1c + +docker run --name canal \ +-p 11111:11111 -d \ +-v $PWD/conf:/home/admin/canal-server/conf \ +-v $PWD/logs:/home/admin/canal-server/logs \ +--net cloud_service \ +canal/canal-server + +docker run -d --name jenkins \ +-p 9090:8080 -p 50000:50000 \ +--restart=always \ +-u root \ +-v $PWD/attach:/root/attach \ +-v $PWD/data:/var/jenkins_home \ +jenkins/jenkins:jdk11 + +docker run --rm -e "ALIYUN_AK=LTAI5tG1noFiFSLDhpTM9Epm" -e "ALIYUN_SK=SEpCZJfZvRNgLqAUWgzmnfSqzPXChR" -e "EMAIL=antordragon@163.com" -v /data/cerbot/certbot-dns-aliyun/cert/:/etc/letsencrypt/ certbot obtain_cert -d "yoyogame.top" -d "*.yoyogame.top" + +docker run --rm -v /data/cerbot/log/:/var/log/letsencrypt -v /data/cerbot/certbot-dns-aliyun/cert/:/etc/letsencrypt/ certbot renew_certs + +```text +keytool -genkey -alias undertow -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -dname "CN=localhost, OU=localhost, O=localhost, L=Wuhan, ST=Hubei, C=CN" +输入密钥库口令: +再次输入新口令: +``` + +| Host | User | plugin | authentication_string | ++-----------+------------------+-----------------------+------------------------------------------------------------------------+ +| localhost | mysql.infoschema | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED | +| localhost | mysql.session | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED | +| localhost | mysql.sys | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED | +| localhost | root | caching_sha2_password | | + + +jvm 查看 +```text +/root/home/docker/images/cloud2/amazon-jdk-11/bin/jstat -gcutil/-gc 3041(pid) 5000(ms) 20(打印次数) --打印gc +保存java进程内存占用情况的基准版本 +/root/home/docker/images/cloud2/amazon-jdk-11/bin/jcmd 21309 VM.native_memory scale=MB baseline +与基准版本进行比较(若怀疑存在内存泄漏,可过段时间再执行观察) +/root/home/docker/images/cloud2/amazon-jdk-11/bin/jcmd 21309 VM.native_memory scale=MB summary.diff +堆中对象概览, 前50 +/root/home/docker/images/cloud2/amazon-jdk-11/bin/jmap -histo 21309 | head -50 +堆信息概览 +/root/home/docker/images/cloud2/amazon-jdk-11/bin/jhsdb jmap --heap --pid 21309 +查看jvm 启动参数 +/root/home/docker/images/cloud2/amazon-jdk-11/bin/jinfo -flags 21309 + +``` + +-XX:CICompilerCount=3 +-XX:CompressedClassSpaceSize=260046848 +-XX:ConcGCThreads=1 +-XX:G1ConcRefinementThreads=4 +-XX:G1HeapRegionSize=1048576 +-XX:GCDrainStackTargetSize=64 +-XX:+HeapDumpOnOutOfMemoryError +-XX:HeapDumpPath=/root/home/docker/images/cloud2/fast +-XX:InitialHeapSize=2147483648 +-XX:MarkStackSize=4194304 +-XX:MaxDirectMemorySize=1073741824 +-XX:MaxHeapSize=2147483648 +-XX:MaxMetaspaceSize=268435456 +-XX:MaxNewSize=1287651328 +-XX:MinHeapDeltaBytes=1048576 +-XX:NativeMemoryTracking=summary +-XX:NonNMethodCodeHeapSize=5830732 +-XX:NonProfiledCodeHeapSize=122913754 +-XX:+PrintGCDetails +-XX:ProfiledCodeHeapSize=122913754 +-XX:ReservedCodeCacheSize=251658240 +-XX:+SegmentedCodeCache +-XX:ThreadStackSize=256 +-XX:+UseCompressedClassPointers +-XX:+UseCompressedOops +-XX:+UseFastUnorderedTimeStamps +-XX:+UseG1GC + +### 只排除配置文件打包 +```xml + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/application.yml + **/bootstrap.yml + **/logback-spring.xml + **/logback.xml + + + + true + + lib/ + + false + + com.chushang.xxx + + + + ./config/ + + + ${project.build.directory} + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.springframework.boot + spring-boot-maven-plugin + + ${project.build.finalName} + + true + + + + + + repackage + + + + + + + maven-resources-plugin + + + copy-resources + package + + copy-resources + + + + + src/main/resources + + application.yml + bootstrap.yml + logback-spring.xml + logback.xml + + true + + + + + UTF-8 + ${project.build.directory}/config + + + + + + ${project.artifactId} + +``` \ No newline at end of file diff --git a/chushang-centrale/chushang-canal/.gitignore b/chushang-centrale/chushang-canal/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-centrale/chushang-canal/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-centrale/chushang-canal/pom.xml b/chushang-centrale/chushang-canal/pom.xml new file mode 100644 index 0000000..abe0c56 --- /dev/null +++ b/chushang-centrale/chushang-canal/pom.xml @@ -0,0 +1,42 @@ + + + + chushang-centrale + com.chushang + 1.0.0 + + 4.0.0 + + chushang-canal + 基于 canal 的数据同步,mysql -> redis + 此处的仅供演示, 具体的应在对应的 项目中, 而非单独启动一个项目 + + + + 17 + 17 + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + com.chushang + chushang-common-canal + + + com.chushang + chushang-common-redis + + + + \ No newline at end of file diff --git a/chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/SanyiCanalApplication.java b/chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/SanyiCanalApplication.java new file mode 100644 index 0000000..17bc9d5 --- /dev/null +++ b/chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/SanyiCanalApplication.java @@ -0,0 +1,18 @@ +package com.chushang.canal; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @author by zhaowenyuan create 2022/8/15 16:14 + */ +@EnableDiscoveryClient +@SpringBootApplication +public class SanyiCanalApplication { + + public static void main(String[] args) { + SpringApplication.run(SanyiCanalApplication.class, args); + } + +} diff --git a/chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/sync/SystemProjectConfigListen.java b/chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/sync/SystemProjectConfigListen.java new file mode 100644 index 0000000..4a776e4 --- /dev/null +++ b/chushang-centrale/chushang-canal/src/main/java/com/chushang/canal/sync/SystemProjectConfigListen.java @@ -0,0 +1,122 @@ +package com.chushang.canal.sync; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.otter.canal.protocol.CanalEntry; +import com.chushang.common.canal.event.CanalEventListener; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author by zhaowenyuan create 2022/Ca6 15:45 + * 项目 公共配置修改 + */ +@Slf4j +@Component +public class SystemProjectConfigListen implements CanalEventListener { + + @Autowired + RedissonClient redissonClient; + + /** + * 项目 公共配置 key 前缀 值 + */ + private static final String PROJECT_CONFIG_KEY = "PROJECT-ID:"; + + /** + * 新增会有 + * @param rowData rowData + */ + @Override + public void onInsert(CanalEntry.RowData rowData) { + // + log.info("insert"); + convert(rowData.getAfterColumnsList()); + } + + @Override + public void onUpdate(CanalEntry.RowData rowData) { + // + log.info("update"); + convert(rowData.getAfterColumnsList()); + } + + + /** + * 理论上 不会有删除 + * @param rowData rowData + */ + @Override + public void onDelete(CanalEntry.RowData rowData) { + // + log.info("delete"); + convert(rowData.getAfterColumnsList()); + } + + /** + * 当前 实现对应的表名 + */ + @Override + public String tableName() { + return "sanyi_system_config"; + } + + @Override + public String schemaName() { + return "sanyi_manager"; + } + + private void convert(List afterColumnsList){ + if (CollectionUtil.isNotEmpty(afterColumnsList)){ + Map columnMap = + afterColumnsList.stream().collect( + Collectors.toMap(CanalEntry.Column::getName, + CanalEntry.Column::getValue, + (k1, k2) -> k2 + ) + ); + + log.info("{}", columnMap); + // 如果 + if (CollectionUtil.isNotEmpty(columnMap)){ + String config_key = columnMap.get("config_key"); + // value 可能为空 + String config_value = columnMap.get("config_value"); + String del_state = columnMap.get("del_state"); + // 任意一个为空, 则不进行继续 + if (StrUtil.hasEmpty(config_key, del_state)){ + return; + } + String[] split = config_key.split(":"); + // 长度不为2时, 代表必定不是所需要的数据 + if (split.length != 2){ + return; + } + String projectId = split[0]; + String key = split[1]; + // 0 时, 代表未删除 true 代表未删除 + boolean flag = "0".equals(del_state); + updateConfig(projectId, key, flag ? config_value : null); + } + } + } + + private void updateConfig(String projectId, String key, Object value){ + RMap map = redissonClient.getMap(PROJECT_CONFIG_KEY + projectId); + if (map.containsKey(key)){ + if (null == value){ + map.remove(key); + return; + } + } + map.put(key, value); + } + +} diff --git a/chushang-centrale/chushang-canal/src/main/resources/bootstrap.yml b/chushang-centrale/chushang-canal/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..a1fd9dd --- /dev/null +++ b/chushang-centrale/chushang-canal/src/main/resources/bootstrap.yml @@ -0,0 +1,47 @@ +server: + port: 1234 +spring: + application: + name: @artifactId@ + cloud: + nacos: + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + group: ${nacos.group} + metadata: + user: + name: sanyi + password: sanyiAdmin@.1 + management: + context-path: ${server.servlet.context-path}/actuator + config: + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + server-addr: ${spring.cloud.nacos.discovery.server-addr} + file-extension: yml +# shared-configs: +# - dataId: application-admin-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +# group: COMMON_GROUP +# - dataId: application-log-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +# group: COMMON_GROUP +# refresh: true + profiles: + active: @profiles.active@ + main: + allow-bean-definition-overriding: true +info: + version: @project.version@ +canal: + client: + instances: + example: + host: 192.168.2.254 + port: 11111 + batchSize: 1024 + # 过滤 dbName.tableName * 代表全部 + filter: sanyi_manager.* + errorRetry: true + errorRetryTime: 60000 + + diff --git a/chushang-centrale/chushang-canal/src/main/resources/logback-nacos.xml b/chushang-centrale/chushang-canal/src/main/resources/logback-nacos.xml new file mode 100644 index 0000000..b1bbb9e --- /dev/null +++ b/chushang-centrale/chushang-canal/src/main/resources/logback-nacos.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + UTF-8 + + + + + ${log.path}/info.log + + ${log.path}/info.%d{yyyy-MM-dd}-%i.log.zip + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + info + + + + + ${log.path}/debug.log + + ${log.path}/debug.%d{yyyy-MM-dd}-%i.log.zip + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + debug + + + + + ${log.path}/error.log + + ${log.path}/error.%d{yyyy-MM-dd}-%i.log.zip + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chushang-centrale/pom.xml b/chushang-centrale/pom.xml new file mode 100644 index 0000000..f50bb1e --- /dev/null +++ b/chushang-centrale/pom.xml @@ -0,0 +1,24 @@ + + + + chushangcloud + com.chushang + 1.0.0 + + 4.0.0 + + chushang-centrale + pom + 叁一中间件部分 -- 可能仅仅只是针对部分表的 + + chushang-canal + + + + 17 + 17 + + + \ No newline at end of file diff --git a/chushang-common/.gitignore b/chushang-common/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-common/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-common/chushang-common-bom/pom.xml b/chushang-common/chushang-common-bom/pom.xml new file mode 100644 index 0000000..e76b677 --- /dev/null +++ b/chushang-common/chushang-common-bom/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + com.chushang + chushang-common-bom + ${commonb.version} + pom + + + 17 + 17 + UTF-8 + 1.0.0 + + + + + com.chushang + chushang-common-canal + ${commonb.version} + + + com.chushang + chushang-common-core + ${commonb.version} + + + com.chushang + chushang-common-easy-es + ${commonb.version} + + + com.chushang + chushang-common-excel + ${commonb.version} + + + com.chushang + chushang-common-feign + ${commonb.version} + + + com.chushang + chushang-common-mail + ${commonb.version} + + + com.chushang + chushang-common-mongo + ${commonb.version} + + + com.chushang + chushang-common-mybatis + ${commonb.version} + + + com.chushang + chushang-common-mybatis-join + ${commonb.version} + + + com.chushang + chushang-common-oss + ${commonb.version} + + + com.chushang + chushang-common-log + ${commonb.version} + + + com.chushang + chushang-common-job + ${commonb.version} + + + com.chushang + chushang-common-redis + ${commonb.version} + + + com.chushang + chushang-common-security + ${commonb.version} + + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-canal/pom.xml b/chushang-common/chushang-common-canal/pom.xml new file mode 100644 index 0000000..4838dbe --- /dev/null +++ b/chushang-common/chushang-common-canal/pom.xml @@ -0,0 +1,33 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common-canal + + + + com.chushang + chushang-common-core + + + com.alibaba.otter + canal.client + + + com.alibaba.otter + canal.protocol + + + org.springframework.boot + spring-boot-configuration-processor + true + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/CanalEventListener.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/CanalEventListener.java new file mode 100644 index 0000000..fd1f55b --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/CanalEventListener.java @@ -0,0 +1,24 @@ +package com.chushang.common.canal.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * inject the present class to the spring context + * as a listener of the canal event + * + * @author chen.qian + * @date 2018/3/19 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface CanalEventListener { + + @AliasFor(annotation = Component.class) + String value() default ""; + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/DeleteListenPoint.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/DeleteListenPoint.java new file mode 100644 index 0000000..246e399 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/DeleteListenPoint.java @@ -0,0 +1,45 @@ +package com.chushang.common.canal.annotation; + +import com.alibaba.otter.canal.protocol.CanalEntry; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * ListenPoint for delete + * + * @author chen.qian + * @date 2018/3/19 + */ + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ListenPoint(eventType = CanalEntry.EventType.DELETE) +public @interface DeleteListenPoint { + + /** + * canal destination + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String destination() default ""; + + /** + * database schema which you are concentrate on + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String[] schema() default {}; + + /** + * tables which you are concentrate on + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String[] table() default {}; + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/InsertListenPoint.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/InsertListenPoint.java new file mode 100644 index 0000000..8c477a7 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/InsertListenPoint.java @@ -0,0 +1,45 @@ +package com.chushang.common.canal.annotation; + +import com.alibaba.otter.canal.protocol.CanalEntry; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * ListenPoint for insert + * + * @author chen.qian + * @date 2018/3/19 + */ + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ListenPoint(eventType = CanalEntry.EventType.INSERT) +public @interface InsertListenPoint { + + /** + * canal destination + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String destination() default ""; + + /** + * database schema which you are concentrate on + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String[] schema() default {}; + + /** + * tables which you are concentrate on + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String[] table() default {}; + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/ListenPoint.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/ListenPoint.java new file mode 100644 index 0000000..a5b4d1a --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/ListenPoint.java @@ -0,0 +1,48 @@ +package com.chushang.common.canal.annotation; + +import com.alibaba.otter.canal.protocol.CanalEntry; + +import java.lang.annotation.*; + +/** + * used to indicate that method(or methods) is(are) the candidate of the + * canal event distributor + * + * @author chen.qian + * @date 2018/3/19 + */ + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ListenPoint { + + /** + * canal destination + * default for all + * @return canal destination + */ + String destination() default ""; + + /** + * database schema which you are concentrate on + * default for all + * @return canal destination + */ + String[] schema() default {}; + + /** + * tables which you are concentrate on + * default for all + * @return canal destination + */ + String[] table() default {}; + + /** + * canal event type + * default for all + * @return canal event type + */ + CanalEntry.EventType[] eventType() default {}; + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/UpdateListenPoint.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/UpdateListenPoint.java new file mode 100644 index 0000000..9fe7fd5 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/annotation/UpdateListenPoint.java @@ -0,0 +1,45 @@ +package com.chushang.common.canal.annotation; + +import com.alibaba.otter.canal.protocol.CanalEntry; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * ListenPoint for update + * + * @author chen.qian + * @date 2018/3/19 + */ + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ListenPoint(eventType = CanalEntry.EventType.UPDATE) +public @interface UpdateListenPoint { + + /** + * canal destination + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String destination() default ""; + + /** + * database schema which you are concentrate on + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String[] schema() default {}; + + /** + * tables which you are concentrate on + * default for all + * @return canal destination + */ + @AliasFor(annotation = ListenPoint.class) + String[] table() default {}; + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/AbstractCanalClient.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/AbstractCanalClient.java new file mode 100644 index 0000000..4c6b05c --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/AbstractCanalClient.java @@ -0,0 +1,108 @@ +package com.chushang.common.canal.client; + +import com.alibaba.otter.canal.client.CanalConnector; +import com.alibaba.otter.canal.client.CanalConnectors; +import com.alibaba.otter.canal.protocol.exception.CanalClientException; +import com.chushang.common.canal.client.transfer.TransponderFactory; +import com.chushang.common.canal.config.CanalConfig; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public abstract class AbstractCanalClient implements CanalClient { + + /** + * running flag + */ + private volatile boolean running; + + /** + * customer config + */ + private final CanalConfig canalConfig; + + + /** + * TransponderFactory + */ + protected final TransponderFactory factory; + + AbstractCanalClient(CanalConfig canalConfig, TransponderFactory factory) { + Objects.requireNonNull(canalConfig, "canalConfig can not be null!"); + Objects.requireNonNull(canalConfig, "transponderFactory can not be null!"); + this.canalConfig = canalConfig; + this.factory = factory; + } + + @Override + public void start() { + Map instanceMap = getConfig(); + for (Map.Entry instanceEntry : instanceMap.entrySet()) { + process(processInstanceEntry(instanceEntry), instanceEntry); + } + + } + + /** + * To initialize the canal connector + * @param connector CanalConnector + * @param config config + */ + protected abstract void process(CanalConnector connector, Map.Entry config); + + private CanalConnector processInstanceEntry(Map.Entry instanceEntry) { + CanalConfig.Instance instance = instanceEntry.getValue(); + CanalConnector connector; + if (instance.isClusterEnabled()) { + List addresses = new ArrayList<>(); + for (String s : instance.getZookeeperAddress()) { + String[] entry = s.split(":"); + if (entry.length != 2) + throw new CanalClientException("error parsing zookeeper address:" + s); + addresses.add(new InetSocketAddress(entry[0], Integer.parseInt(entry[1]))); + } + connector = CanalConnectors.newClusterConnector(addresses, instanceEntry.getKey(), + instance.getUserName(), + instance.getPassword()); + } else { + connector = CanalConnectors.newSingleConnector(new InetSocketAddress(instance.getHost(), instance.getPort()), + instanceEntry.getKey(), + instance.getUserName(), + instance.getPassword()); + } + return connector; + } + + /** + * get the config + * + * @return config + */ + protected Map getConfig() { + CanalConfig config = canalConfig; + Map instanceMap; + if (config != null && (instanceMap = config.getInstances()) != null && !instanceMap.isEmpty()) { + return config.getInstances(); + } else { + throw new CanalClientException("can not get the configuration of canal client!"); + } + } + + @Override + public void stop() { + setRunning(false); + } + + @Override + public boolean isRunning() { + return running; + } + + private void setRunning(boolean running) { + this.running = running; + } +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/CanalClient.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/CanalClient.java new file mode 100644 index 0000000..05531d4 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/CanalClient.java @@ -0,0 +1,22 @@ +package com.chushang.common.canal.client; + +public interface CanalClient { + + /** + * open the canal client + * to get the config and connect to the canal server (1 : 1 or 1 : n) + * and then transfer the event to the special listener + * */ + void start(); + + /** + * stop the client + */ + void stop(); + + /** + * is running + * @return yes or no + */ + boolean isRunning(); +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/ListenerPoint.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/ListenerPoint.java new file mode 100644 index 0000000..ed030f5 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/ListenerPoint.java @@ -0,0 +1,25 @@ +package com.chushang.common.canal.client; + +import com.chushang.common.canal.annotation.ListenPoint; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +public class ListenerPoint { + private final Object target; + private final Map invokeMap = new HashMap<>(); + + ListenerPoint(Object target, Method method, ListenPoint anno) { + this.target = target; + this.invokeMap.put(method, anno); + } + + public Object getTarget() { + return target; + } + + public Map getInvokeMap() { + return invokeMap; + } +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/SimpleCanalClient.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/SimpleCanalClient.java new file mode 100644 index 0000000..7fd010a --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/SimpleCanalClient.java @@ -0,0 +1,85 @@ +package com.chushang.common.canal.client; + +import com.alibaba.otter.canal.client.CanalConnector; +import com.chushang.common.canal.annotation.CanalEventListener; +import com.chushang.common.canal.util.BeanUtil; +import com.chushang.common.canal.annotation.ListenPoint; +import com.chushang.common.canal.client.transfer.TransponderFactory; +import com.chushang.common.canal.config.CanalConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class SimpleCanalClient extends AbstractCanalClient { + private final static Logger logger = LoggerFactory.getLogger(SimpleCanalClient.class); + + /** + * executor + */ + private final ThreadPoolExecutor executor; + + /** + * listeners which are used by implementing the Interface + */ + private final List listeners = new ArrayList<>(); + + /** + * listeners which are used by annotation + */ + private final List annoListeners = new ArrayList<>(); + + public SimpleCanalClient(CanalConfig canalConfig, TransponderFactory factory) { + super(canalConfig, factory); + executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue<>(), Executors.defaultThreadFactory()); + initListeners(); + } + + @Override + protected void process(CanalConnector connector, Map.Entry config) { + executor.submit(factory.newTransponder(connector, config, listeners, annoListeners)); + } + + @Override + public void stop() { + super.stop(); + executor.shutdown(); + } + + /** + * init listeners + */ + private void initListeners() { + logger.info("{}: initializing the listeners....", Thread.currentThread().getName()); + List list = BeanUtil.getBeansOfType(com.chushang.common.canal.event.CanalEventListener.class); + if (list != null) { + listeners.addAll(list); + } + Map listenerMap = BeanUtil.getBeansWithAnnotation(CanalEventListener.class); + if (listenerMap != null) { + for (Object target : listenerMap.values()) { + Method[] methods = target.getClass().getDeclaredMethods(); + for (Method method : methods) { + ListenPoint l = AnnotationUtils.findAnnotation(method, ListenPoint.class); + if (l != null) { + annoListeners.add(new ListenerPoint(target, method, l)); + } + } + } + } + logger.info("{}: initializing the listeners end.", Thread.currentThread().getName()); + if (logger.isWarnEnabled() && listeners.isEmpty() && annoListeners.isEmpty()) { + logger.warn("{}: No listener found in context! ", Thread.currentThread().getName()); + } + } +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractBasicMessageTransponder.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractBasicMessageTransponder.java new file mode 100644 index 0000000..6361bf3 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractBasicMessageTransponder.java @@ -0,0 +1,158 @@ +package com.chushang.common.canal.client.transfer; + +import com.alibaba.otter.canal.client.CanalConnector; +import com.alibaba.otter.canal.protocol.CanalEntry; +import com.alibaba.otter.canal.protocol.exception.CanalClientException; +import com.chushang.common.canal.config.CanalConfig; +import com.chushang.common.canal.event.CanalEventListener; +import com.chushang.common.canal.annotation.ListenPoint; +import com.chushang.common.canal.client.ListenerPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public abstract class AbstractBasicMessageTransponder extends AbstractMessageTransponder { + + private final static Logger logger = LoggerFactory.getLogger(AbstractBasicMessageTransponder.class); + + public AbstractBasicMessageTransponder(CanalConnector connector, Map.Entry config, List listeners, List annoListeners) { + super(connector, config, listeners, annoListeners); + } + + + @Override + protected void distributeEvent(List entryList) { + for (CanalEntry.Entry entry : entryList) { + //ignore the transaction operations + List ignoreEntryTypes = getIgnoreEntryTypes(); + if (ignoreEntryTypes != null + && ignoreEntryTypes.stream().anyMatch(t -> entry.getEntryType() == t)) { + continue; + } + CanalEntry.RowChange rowChange; + try { + rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); + } catch (Exception e) { + throw new CanalClientException("ERROR ## parser of event has an error , data:" + entry.toString(), + e); + } + //ignore the ddl operation + if (rowChange.hasIsDdl() && rowChange.getIsDdl()) { + processDdl(rowChange); + continue; + } + CanalEntry.Header header = entry.getHeader(); + String tableName = header.getTableName(); + String schemaName = header.getSchemaName(); + for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { + //distribute to listener interfaces + distributeByImpl(rowChange.getEventType(),schemaName ,tableName, rowData); + //distribute to annotation listener interfaces + distributeByAnnotation(destination, + schemaName, + tableName, + rowChange.getEventType(), + rowData); + } + } + } + + /** + * process the ddl event + * @param rowChange rowChange + */ + protected void processDdl(CanalEntry.RowChange rowChange) {} + + /** + * distribute to listener interfaces + * + * @param eventType eventType + * @param schemaName 指定数据库名称 --> 一般由filter 指定, 此处不应当判断 + * @param tableName 指定数据表名 + * @param rowData rowData + */ + protected void distributeByImpl(CanalEntry.EventType eventType, String schemaName, String tableName, CanalEntry.RowData rowData) { + logger.info("schemaName : {}, tableName : {}", schemaName, tableName); + if (listeners != null) { + for (CanalEventListener listener : listeners) { + if (tableName.equals(listener.tableName()) && schemaName.equals(listener.schemaName())){ + listener.onEvent(eventType, rowData); + } + } + } + } + + /** + * distribute to annotation listener interfaces + * + * @param destination destination + * @param schemaName schema + * @param tableName table name + * @param eventType event type + * @param rowData row data + */ + protected void distributeByAnnotation(String destination, + String schemaName, + String tableName, + CanalEntry.EventType eventType, + CanalEntry.RowData rowData) { + //invoke the listeners + annoListeners.forEach(point -> point + .getInvokeMap() + .entrySet() + .stream() + .filter(getAnnotationFilter(destination, schemaName, tableName, eventType)) + .forEach(entry -> { + Method method = entry.getKey(); + method.setAccessible(true); + try { + Object[] args = getInvokeArgs(method, eventType, rowData); + method.invoke(point.getTarget(), args); + } catch (Exception e) { + logger.error("{}: Error occurred when invoke the listener's interface! class:{}, method:{}", + Thread.currentThread().getName(), + point.getTarget().getClass().getName(), method.getName()); + } + })); + } + + /** + * get the filters predicate + * + * @param destination destination + * @param schemaName schema + * @param tableName table name + * @param eventType event type + * @return predicate + */ + protected abstract Predicate> getAnnotationFilter(String destination, + String schemaName, + String tableName, + CanalEntry.EventType eventType); + + /** + * get the args + * + * @param method method + * @param eventType event type + * @param rowData row data + * @return args which will be used by invoking the annotation methods + */ + protected abstract Object[] getInvokeArgs(Method method, CanalEntry.EventType eventType, + CanalEntry.RowData rowData); + + /** + * get the ignore eventType list + * + * @return eventType list + */ + protected List getIgnoreEntryTypes() { + return Collections.emptyList(); + } + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractMessageTransponder.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractMessageTransponder.java new file mode 100644 index 0000000..1cc73c1 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/AbstractMessageTransponder.java @@ -0,0 +1,157 @@ +package com.chushang.common.canal.client.transfer; + +import com.alibaba.otter.canal.client.CanalConnector; +import com.alibaba.otter.canal.protocol.CanalEntry; +import com.alibaba.otter.canal.protocol.Message; +import com.alibaba.otter.canal.protocol.exception.CanalClientException; +import com.chushang.common.canal.client.ListenerPoint; +import com.chushang.common.canal.config.CanalConfig; +import com.chushang.common.canal.event.CanalEventListener; +import com.chushang.common.core.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public abstract class AbstractMessageTransponder extends MessageTransponder { + + /** + * canal connector + */ + private final CanalConnector connector; + + /** + * custom config + */ + protected final CanalConfig.Instance config; + + /** + * destination of canal server + */ + protected final String destination; + + /** + * listeners which are used by implementing the Interface + */ + protected final List listeners = new ArrayList<>(); + + /** + * listeners which are used by annotation + */ + protected final List annoListeners = new ArrayList<>(); + + /** + * running flag + */ + private volatile boolean running = true; + + private static final Logger logger = LoggerFactory.getLogger(AbstractMessageTransponder.class); + + public AbstractMessageTransponder(CanalConnector connector, + Map.Entry config, + List listeners, + List annoListeners) { + Objects.requireNonNull(connector, "connector can not be null!"); + Objects.requireNonNull(config, "config can not be null!"); + this.connector = connector; + this.destination = config.getKey(); + this.config = config.getValue(); + if (listeners != null) + this.listeners.addAll(listeners); + if (annoListeners != null) + this.annoListeners.addAll(annoListeners); + } + + @Override + public void run() { + // 在 run 时 才进行连接 + connect(); + int errorCount = config.getRetryCount(); + final long interval = config.getAcquireInterval(); + final String threadName = Thread.currentThread().getName(); + boolean interrupted = Thread.currentThread().isInterrupted(); + do { + try { + Message message = connector.getWithoutAck(config.getBatchSize()); + long batchId = message.getId(); + int size = message.getEntries().size(); + if (logger.isDebugEnabled()) { + logger.debug("{}: Get message from canal server >>>>> size:{}", threadName, size); + } + //empty message + if (batchId == -1 || size == 0) { + if (logger.isDebugEnabled()) { + logger.debug("{}: Empty message... sleep for {} millis", threadName, interval); + } + Thread.sleep(interval); + } else { + distributeEvent(message.getEntries()); + } + + // commit ack + connector.ack(batchId); + if (logger.isDebugEnabled()) { + logger.debug("{}: Ack message. batchId:{}", threadName, batchId); + } + } catch (CanalClientException e) { + errorCount--; + logger.error(threadName + ": Error occurred!! ", e); + try { + Thread.sleep(interval); + } catch (InterruptedException e1) { + errorCount = 0; + } + } catch (InterruptedException e) { + errorCount = 0; + connector.rollback(); + } finally { + // 重试次数 为0, 不在进行重试, 等待重新连接 + if (errorCount <= 0) { + stop(); + logger.info("{}: Topping the client.. ", Thread.currentThread().getName()); + } + } + } while ((running && !interrupted)); + } + + protected abstract void distributeEvent(List entryList); + + private void connect() { + connector.connect(); + // 此处 添加 过滤 -> + if (StringUtils.isNotEmpty(config.getFilter())) { + connector.subscribe(config.getFilter()); + } else { + connector.subscribe(); + } + connector.rollback(); + + logger.info("connector is connect"); + } + + /** + * stop running + */ + void stop() { + // 此处应当就是失败了, 需要停止进行重试 + logger.info("{}: client stopped. ", Thread.currentThread().getName()); + running = false; + if (null == connector) { + return; + } + // 停止时, 关闭连接 + connector.disconnect(); + // 说明失败重试 + if (config.isErrorRetry()) { + logger.info("connector restart"); + + long errorRetryTime = config.getErrorRetryTime(); + running = true; + // 延时执行 + timer.schedule(this, errorRetryTime); + } + } +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultMessageTransponder.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultMessageTransponder.java new file mode 100644 index 0000000..16429fb --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultMessageTransponder.java @@ -0,0 +1,68 @@ +package com.chushang.common.canal.client.transfer; + +import com.alibaba.otter.canal.client.CanalConnector; +import com.alibaba.otter.canal.protocol.CanalEntry; +import com.chushang.common.canal.config.CanalConfig; +import com.chushang.common.canal.annotation.ListenPoint; +import com.chushang.common.canal.client.ListenerPoint; +import com.chushang.common.canal.event.CanalEventListener; +import com.chushang.common.core.util.StringUtils; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public class DefaultMessageTransponder extends AbstractBasicMessageTransponder { + + public DefaultMessageTransponder(CanalConnector connector, + Map.Entry config, + List listeners, + List annoListeners) { + super(connector, config, listeners, annoListeners); + } + + /** + * get the filters predicate + * + * @param destination destination + * @param schemaName schema + * @param tableName table name + * @param eventType event type + * @return predicate + */ + @Override + protected Predicate> getAnnotationFilter(String destination, + String schemaName, + String tableName, + CanalEntry.EventType eventType) { + Predicate> df = e -> StringUtils.isEmpty(e.getValue().destination()) + || e.getValue().destination().equals(destination); + + Predicate> sf = e -> e.getValue().schema().length == 0 + || Arrays.asList(e.getValue().schema()).contains(schemaName); + + Predicate> tf = e -> e.getValue().table().length == 0 + || Arrays.asList(e.getValue().table()).contains(tableName); + + Predicate> ef = e -> (e.getValue().eventType().length == 0) + || Arrays.stream(e.getValue().eventType()).anyMatch(ev -> ev == eventType); + return df.and(sf).and(tf).and(ef); + } + + @Override + protected Object[] getInvokeArgs(Method method, CanalEntry.EventType eventType, CanalEntry.RowData rowData) { + return Arrays.stream(method.getParameterTypes()) + .map(p -> p == CanalEntry.EventType.class + ? eventType + : p == CanalEntry.RowData.class + ? rowData : null) + .toArray(); + } + + @Override + protected List getIgnoreEntryTypes() { + return Arrays.asList(CanalEntry.EntryType.TRANSACTIONBEGIN, CanalEntry.EntryType.TRANSACTIONEND, CanalEntry.EntryType.HEARTBEAT); + } +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultTransponderFactory.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultTransponderFactory.java new file mode 100644 index 0000000..8532ff3 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/DefaultTransponderFactory.java @@ -0,0 +1,17 @@ +package com.chushang.common.canal.client.transfer; + +import com.alibaba.otter.canal.client.CanalConnector; +import com.chushang.common.canal.config.CanalConfig; +import com.chushang.common.canal.client.ListenerPoint; +import com.chushang.common.canal.event.CanalEventListener; + +import java.util.List; +import java.util.Map; + +public class DefaultTransponderFactory implements TransponderFactory { + @Override + public MessageTransponder newTransponder(CanalConnector connector, Map.Entry config, List listeners, + List annoListeners) { + return new DefaultMessageTransponder(connector, config, listeners, annoListeners); + } +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponder.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponder.java new file mode 100644 index 0000000..50a1bed --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponder.java @@ -0,0 +1,10 @@ +package com.chushang.common.canal.client.transfer; + +import java.util.Timer; +import java.util.TimerTask; + +public abstract class MessageTransponder extends TimerTask { + + public static final Timer timer = new Timer(); + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponders.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponders.java new file mode 100644 index 0000000..939beea --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/MessageTransponders.java @@ -0,0 +1,9 @@ +package com.chushang.common.canal.client.transfer; + +public class MessageTransponders { + + public static TransponderFactory defaultMessageTransponder() { + return new DefaultTransponderFactory(); + } + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/TransponderFactory.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/TransponderFactory.java new file mode 100644 index 0000000..79a9f3d --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/client/transfer/TransponderFactory.java @@ -0,0 +1,22 @@ +package com.chushang.common.canal.client.transfer; + +import com.alibaba.otter.canal.client.CanalConnector; +import com.chushang.common.canal.client.ListenerPoint; +import com.chushang.common.canal.config.CanalConfig; +import com.chushang.common.canal.event.CanalEventListener; + +import java.util.List; +import java.util.Map; + +public interface TransponderFactory { + + /** + * @param connector connector + * @param config config + * @param listeners listeners + * @param annoListeners annoListeners + * @return MessageTransponder + */ + MessageTransponder newTransponder(CanalConnector connector, Map.Entry config, List listeners, + List annoListeners); +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalClientAutoConfiguration.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalClientAutoConfiguration.java new file mode 100644 index 0000000..7b29e8a --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalClientAutoConfiguration.java @@ -0,0 +1,43 @@ +package com.chushang.common.canal.config; + + +import com.chushang.common.canal.client.SimpleCanalClient; +import com.chushang.common.canal.client.transfer.MessageTransponders; +import com.chushang.common.canal.util.BeanUtil; +import com.chushang.common.canal.client.CanalClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + + +public class CanalClientAutoConfiguration { + + private final static Logger logger = LoggerFactory.getLogger(CanalClientAutoConfiguration.class); + + @Autowired + private CanalConfig canalConfig; + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public BeanUtil beanUtil() { + return new BeanUtil(); + } + + @Bean + private CanalClient canalClient() { + + CanalClient canalClient = + new SimpleCanalClient(canalConfig, MessageTransponders.defaultMessageTransponder()); + + canalClient.start(); + + logger.info("Starting canal client...."); + + return canalClient; + } + + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalConfig.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalConfig.java new file mode 100644 index 0000000..c3c34dc --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/config/CanalConfig.java @@ -0,0 +1,200 @@ +package com.chushang.common.canal.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +@Order(Ordered.HIGHEST_PRECEDENCE) +@RefreshScope +@Configuration +@ConfigurationProperties(prefix = "canal.client") +public class CanalConfig { + + /** + * instance config + */ + private Map instances = new LinkedHashMap<>(); + + public Map getInstances() { + return instances; + } + + public void setInstances(Map instances) { + this.instances = instances; + } + + /** + * instance config class + */ + public static class Instance { + + /** + * is cluster-mod + */ + private boolean clusterEnabled; + /** + * zookeeper address + */ + private Set zookeeperAddress = new LinkedHashSet<>(); + + /** + * canal server host + */ + private String host = "127.0.0.1"; + + /** + * canal server port + */ + private int port = 10001; + + /** + * canal user name + */ + private String userName = ""; + + /** + * canal password + */ + private String password = ""; + + /** + * size when get messages from the canal server + */ + private int batchSize = 1000; + + /** + * filter + */ + private String filter; + + /** + * retry count when error occurred + * 单次异常 的重试次数 + */ + private int retryCount = 5; + + /** + * 失败后重新启动 默认不进行失败重新连接 + */ + private boolean errorRetry = false; + + /** + * 失败后间隔多长时间进行重启 + * 单位 ms + * 默认10分钟 一次进行重新连接 + */ + private long errorRetryTime = 10 * 60 * 1000; + + /** + * interval of the message-acquiring + */ + private long acquireInterval = 1000; + + public Instance() {} + + public boolean isClusterEnabled() { + return clusterEnabled; + } + + public void setClusterEnabled(boolean clusterEnabled) { + this.clusterEnabled = clusterEnabled; + } + + public Set getZookeeperAddress() { + return zookeeperAddress; + } + + public void setZookeeperAddress(Set zookeeperAddress) { + this.zookeeperAddress = zookeeperAddress; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getBatchSize() { + return batchSize; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } + + public int getRetryCount() { + return retryCount; + } + + public void setRetryCount(int retryCount) { + this.retryCount = retryCount; + } + + public long getAcquireInterval() { + return acquireInterval; + } + + public void setAcquireInterval(long acquireInterval) { + this.acquireInterval = acquireInterval; + } + + public boolean isErrorRetry() { + return errorRetry; + } + + public void setErrorRetry(boolean errorRetry) { + this.errorRetry = errorRetry; + } + + public long getErrorRetryTime() { + return errorRetryTime; + } + + public void setErrorRetryTime(long errorRetryTime) { + this.errorRetryTime = errorRetryTime; + } + + } + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/event/CanalEventListener.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/event/CanalEventListener.java new file mode 100644 index 0000000..01b0fcf --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/event/CanalEventListener.java @@ -0,0 +1,52 @@ +package com.chushang.common.canal.event; + +import com.alibaba.otter.canal.protocol.CanalEntry; + +import java.util.Objects; + +public interface CanalEventListener { + + default void onEvent(CanalEntry.EventType eventType, CanalEntry.RowData rowData) { + Objects.requireNonNull(eventType); + switch (eventType) { + case INSERT: + onInsert(rowData); + break; + case UPDATE: + onUpdate(rowData); + break; + case DELETE: + onDelete(rowData); + break; + default: + break; + } + } + + + /** + * fired on insert event + * + * @param rowData rowData + */ + void onInsert(CanalEntry.RowData rowData); + + /** + * fired on update event + * + * @param rowData rowData + */ + void onUpdate(CanalEntry.RowData rowData); + + /** + * fired on delete event + * + * @param rowData rowData + */ + void onDelete(CanalEntry.RowData rowData); + + String tableName(); + + String schemaName(); + +} diff --git a/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/util/BeanUtil.java b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/util/BeanUtil.java new file mode 100644 index 0000000..c946c52 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/java/com/chushang/common/canal/util/BeanUtil.java @@ -0,0 +1,52 @@ +package com.chushang.common.canal.util; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class BeanUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + BeanUtil.applicationContext = applicationContext; + } + + public static T getBean(Class clazz) { + T obj; + try { + obj = applicationContext.getBean(clazz); + } catch (Exception e) { + obj = null; + } + return obj; + } + + public static List getBeansOfType(Class clazz) { + Map map; + try { + map = applicationContext.getBeansOfType(clazz); + } catch (Exception e) { + map = null; + } + return map == null ? null : new ArrayList<>(map.values()); + } + + public static Map getBeansWithAnnotation(Class anno) { + Map map; + try { + map = applicationContext.getBeansWithAnnotation(anno); + } catch (Exception e) { + map = null; + } + return map; + } +} \ No newline at end of file diff --git a/chushang-common/chushang-common-canal/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-canal/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..7acf598 --- /dev/null +++ b/chushang-common/chushang-common-canal/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.common.canal.config.CanalConfig, \ + com.chushang.common.canal.config.CanalClientAutoConfiguration diff --git a/chushang-common/chushang-common-core/pom.xml b/chushang-common/chushang-common-core/pom.xml new file mode 100644 index 0000000..bca376e --- /dev/null +++ b/chushang-common/chushang-common-core/pom.xml @@ -0,0 +1,65 @@ + + + + chushang-common + com.chushang + 1.0.0 + + + 4.0.0 + + chushang-common-core + + + cn.hutool + hutool-all + + + javax.servlet + javax.servlet-api + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-json + + + org.apache.commons + commons-lang3 + + + + io.jsonwebtoken + jjwt + + + + com.alibaba + transmittable-thread-local + + + org.jboss.xnio + xnio-api + compile + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.fastjson2 + fastjson2 + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/cache/CacheNameSpase.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/cache/CacheNameSpase.java new file mode 100644 index 0000000..d3e6172 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/cache/CacheNameSpase.java @@ -0,0 +1,19 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.core.cache; + +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; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface CacheNameSpase { + String value() default ""; +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/config/JacksonConfiguration.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/config/JacksonConfiguration.java new file mode 100644 index 0000000..94bf128 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/config/JacksonConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.config; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.chushang.common.core.jackson.JavaTimeModule; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.ZoneId; +import java.util.Locale; +import java.util.TimeZone; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ObjectMapper.class) +@AutoConfigureBefore(JacksonAutoConfiguration.class) +public class JacksonConfiguration { + + @Bean + @ConditionalOnMissingBean + public Jackson2ObjectMapperBuilderCustomizer customizer() { + return builder -> { + builder.locale(Locale.CHINA); + builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN); + builder.serializerByType(Long.class, ToStringSerializer.instance); + builder.modules(new JavaTimeModule()); + }; + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CacheConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CacheConstants.java new file mode 100644 index 0000000..12d93de --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CacheConstants.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.constant; + +/** + */ +public interface CacheConstants { + + /** + * 权限缓存前缀 + */ + String LOGIN_TOKEN_KEY = "login_tokens:"; + + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CharConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CharConstants.java new file mode 100644 index 0000000..a9b7a12 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CharConstants.java @@ -0,0 +1,16 @@ +package com.chushang.common.core.constant; + +/** + * by zhaowenyuan create 2022/2/15 18:09 + */ +public interface CharConstants { + char[] chars = new char[]{ + '2','3','4','5','6','7','8','9', + 'a','b','c','d','e','f','g','h','j','k','m','n','o','p','q','r','s','t','u','v','w','x','y','z', + 'A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z' + }; + + char[] numChars = new char[]{ + '0','1','2','3','4','5','6','7','8','9' + }; +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CommonConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CommonConstants.java new file mode 100644 index 0000000..64cb3cc --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/CommonConstants.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.constant; + +public interface CommonConstants { + + String HEAD_TOKEN_KEY = "sanyi-token"; + + String ACCOUNT_SYMBOL = ":"; + + String SEMICOLON = ";"; + + String DOUBLE_QUOTATION_MARKS = "\""; + + String SINGLE_QUOTATION_MARKS = "'"; + + String DOUBLE_SLASH = "\\"; + + String PERCENT_SIGN = "%"; + + String COMMA = ","; + + String AT_SYMBOL = "@"; + + + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/Constants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/Constants.java new file mode 100644 index 0000000..10708f4 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/Constants.java @@ -0,0 +1,145 @@ +package com.chushang.common.core.constant; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public interface Constants +{ + /** + * UTF-8 字符集 + */ + String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + String GBK = "GBK"; + + /** + * RMI 远程方法调用 + */ + String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + String LOOKUP_LDAPS = "ldaps:"; + + /** + * http请求 + */ + String HTTP = "http://"; + + /** + * https请求 + */ + String HTTPS = "https://"; + + String WWW = "www."; + + /** + * 成功标记 + */ + Integer SUCCESS = 200; + + /** + * 失败标记 + */ + Integer FAIL = 500; + + /** + * 登录成功状态 + */ + String LOGIN_SUCCESS_STATUS = "0"; + + /** + * 登录失败状态 + */ + String LOGIN_FAIL_STATUS = "1"; + + /** + * 登录成功 + */ + String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + String LOGOUT = "Logout"; + + /** + * 注册 + */ + String REGISTER = "Register"; + + /** + * 登录失败 + */ + String LOGIN_FAIL = "Error"; + + /** + * 当前记录起始索引 + */ + String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + String IS_ASC = "isAsc"; + + /** + * 验证码 redis key + */ + String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 验证码有效期(分钟) + */ + long CAPTCHA_EXPIRATION = 2; + + + /** + * 参数管理 cache key + */ + String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + String SYS_DICT_KEY = "sys_dict:"; + + /** + * 资源映射路径 前缀 + */ + String RESOURCE_PREFIX = "/profile"; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + String[] JOB_WHITELIST_STR = { "com.ruoyi" }; + + /** + * 定时任务违规的字符 + */ + String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.ruoyi.common.core.utils.file" }; + + String SERIAL_VERSION_STR = "serialVersionUID"; +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/RegularConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/RegularConstants.java new file mode 100644 index 0000000..6371c79 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/RegularConstants.java @@ -0,0 +1,18 @@ +package com.chushang.common.core.constant; + +/** + * @author by zhaowenyuan create 2022/8/2 18:25 + */ +public interface RegularConstants { + + /** + * 邮箱正则匹配 + * 名称允许汉字、字母、数字,域名只允许英文域名 + */ + String ZH_MAIL_REGULAR = "^[A-Za-z0-9\\u4e00-\\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$"; + /** + * 只允许英文字母、数字、下划线、英文句号、以及中划线组成 + */ + String EN_MAIL_REGULAR = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$"; + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/SecurityConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/SecurityConstants.java new file mode 100644 index 0000000..b8c639e --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/SecurityConstants.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.constant; + +/** + * ruoyi + */ +public interface SecurityConstants { + + /** + * 用户ID字段 + */ + String DETAILS_USER_ID = "user_id"; + + /** + * 用户名字段 + */ + String DETAILS_USERNAME = "username"; + + /** + * 授权信息字段 + */ + String AUTHORIZATION_HEADER = "authorization"; + + /** + * 请求来源 + */ + String FROM_SOURCE = "from-source"; + + /** + * 内部请求 + */ + String INNER = "inner"; + + /** + * 用户标识 + */ + String USER_KEY = "user_key"; + + /** + * 登录用户 + */ + String LOGIN_USER = "login_user"; + /** + * 角色权限 + */ + String ROLE_PERMISSION = "role_permission"; + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/ServiceNameConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/ServiceNameConstants.java new file mode 100644 index 0000000..0bac033 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/ServiceNameConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.constant; + +/** + */ +public interface ServiceNameConstants { + + String SYSTEM_SERVICE_V2 = "system-service-v2"; + + String MANAGER_SERVICE = "manager-service"; +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/TokenConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/TokenConstants.java new file mode 100644 index 0000000..12e04b7 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/TokenConstants.java @@ -0,0 +1,36 @@ +package com.chushang.common.core.constant; + +public interface TokenConstants +{ + /** + * 令牌自定义标识 + */ + String AUTHENTICATION = "Authorization"; + /** + * 参数加密值 + */ + String API_SIGN = "apiSign"; + + /** + * 请求时间戳 + */ + String REQUEST_TIME = "requestTime"; + + /** + * 令牌前缀 + */ + String PREFIX = "Bearer "; + + /** + * 令牌秘钥 + */ + String SECRET = "my_name_is_onn_secret"; + + // 1秒 + Long MILLIS_SECOND = 1000L; + // 1分钟 + Long MILLIS_MINUTE = 60L * MILLIS_SECOND; + // 1个小时 + Long MILLIS_HOUR = 60L * MILLIS_MINUTE; + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UnityLanguageConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UnityLanguageConstants.java new file mode 100644 index 0000000..0ad4d5a --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UnityLanguageConstants.java @@ -0,0 +1,180 @@ +package com.chushang.common.core.constant; + +/** + * @author by zhaowenyuan create 2022/7/26 19:01 + */ +public interface UnityLanguageConstants { + // + // 摘要: + // Afrikaans. + int Afrikaans = 0; + // + // 摘要: + // Arabic. + int Arabic = 1; + // + // 摘要: + // Basque. + int Basque = 2; + // + // 摘要: + // Belarusian. + int Belarusian = 3; + // + // 摘要: + // Bulgarian. + int Bulgarian = 4; + // + // 摘要: + // Catalan. + int Catalan = 5; + // + // 摘要: + // Chinese. + int Chinese = 6; + // + // 摘要: + // Czech. + int Czech = 7; + // + // 摘要: + // Danish. + int Danish = 8; + // + // 摘要: + // Dutch. + int Dutch = 9; + // + // 摘要: + // English. + int English = 10; + // + // 摘要: + // Estonian. + int Estonian = 11; + // + // 摘要: + // Faroese. + int Faroese = 12; + // + // 摘要: + // Finnish. + int Finnish = 13; + // + // 摘要: + // French. + int French = 14; + // + // 摘要: + // German. + int German = 15; + // + // 摘要: + // Greek. + int Greek = 16; + // + // 摘要: + // Hebrew. + int Hebrew = 17; + int Hugarian = 18; + // + // 摘要: + // Hungarian. + int Hungarian = 18; + // + // 摘要: + // Icelandic. + int Icelandic = 19; + // + // 摘要: + // Indonesian. + int Indonesian = 20; + // + // 摘要: + // Italian. + int Italian = 21; + // + // 摘要: + // Japanese. + int Japanese = 22; + // + // 摘要: + // Korean. + int Korean = 23; + // + // 摘要: + // Latvian. + int Latvian = 24; + // + // 摘要: + // Lithuanian. + int Lithuanian = 25; + // + // 摘要: + // Norwegian. + int Norwegian = 26; + // + // 摘要: + // Polish. + int Polish = 27; + // + // 摘要: + // Portuguese. + int Portuguese = 28; + // + // 摘要: + // Romanian. + int Romanian = 29; + // + // 摘要: + // Russian. + int Russian = 30; + // + // 摘要: + // Serbo-Croatian. + int SerboCroatian = 31; + // + // 摘要: + // Slovak. + int Slovak = 32; + // + // 摘要: + // Slovenian. + int Slovenian = 33; + // + // 摘要: + // Spanish. + int Spanish = 34; + // + // 摘要: + // Swedish. + int Swedish = 35; + // + // 摘要: + // Thai. + int Thai = 36; + // + // 摘要: + // Turkish. + int Turkish = 37; + // + // 摘要: + // Ukrainian. + int Ukrainian = 38; + // + // 摘要: + // Vietnamese. + int Vietnamese = 39; + // + // 摘要: + // ChineseSimplified. + int ChineseSimplified = 40; + // + // 摘要: + // ChineseTraditional. + int ChineseTraditional = 41; + // + // 摘要: + // Unknown. + int Unknown = 42; +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UserConstants.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UserConstants.java new file mode 100644 index 0000000..58c5efb --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/constant/UserConstants.java @@ -0,0 +1,68 @@ +package com.chushang.common.core.constant; + +/** + * 用户常量信息 + * + * @author ruoyi + */ +public interface UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + String NORMAL = "0"; + + /** 异常状态 */ + String EXCEPTION = "1"; + + /** 用户封禁状态 */ + String USER_DISABLE = "1"; + + /** 角色封禁状态 */ + String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + String YES = "Y"; + + /** 是否菜单外链(是) */ + String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + String TYPE_DIR = "M"; + /** Layout组件标识 */ + String LAYOUT = "Layout"; + /** ParentView组件标识 */ + String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + String INNER_LINK = "InnerLink"; + + /** + * 用户名长度限制 + */ + int USERNAME_MIN_LENGTH = 2; + + int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + int PASSWORD_MIN_LENGTH = 5; + + int PASSWORD_MAX_LENGTH = 20; +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/enums/AppStartType.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/enums/AppStartType.java new file mode 100644 index 0000000..e6b0963 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/enums/AppStartType.java @@ -0,0 +1,34 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.core.enums; + +public enum AppStartType { + main("MAIN", "内嵌方式启动"), + web_server("WEB_SERVER", "部署到容器方式启动"); + + public static final String START_FORMAT = "[{}]开始启动***{}***"; + public static final String END_FORMAT = "[{}]***{}***启动完成"; + public static final String END_ERROR_FORMAT = "[{}]***定时器***异常:{}"; + private final String type; + private final String desc; + + private AppStartType(String type, String desc) { + this.type = type; + this.desc = desc; + } + + public String desc() { + return this.desc; + } + + public boolean eq(String type) { + return this.type().equals(type); + } + + public String type() { + return this.type; + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/CheckedException.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/CheckedException.java new file mode 100644 index 0000000..2e80e63 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/CheckedException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.exception; + +import lombok.NoArgsConstructor; + +/** + * @author lengleng + * @date 😴2018年06月22日16:21:57 + */ +@NoArgsConstructor +public class CheckedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public CheckedException(String message) { + super(message); + } + + public CheckedException(Throwable cause) { + super(cause); + } + + public CheckedException(String message, Throwable cause) { + super(message, cause); + } + + public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/InnerAuthException.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/InnerAuthException.java new file mode 100644 index 0000000..f9f78ea --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/InnerAuthException.java @@ -0,0 +1,16 @@ +package com.chushang.common.core.exception; + +/** + * 内部认证异常 + * + * @author ruoyi + */ +public class InnerAuthException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public InnerAuthException(String message) + { + super(message); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ResultException.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ResultException.java new file mode 100644 index 0000000..cc09551 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ResultException.java @@ -0,0 +1,50 @@ +package com.chushang.common.core.exception; + +import com.chushang.common.core.exception.enums.ExceptionEnum; +import com.chushang.common.core.web.EnumUtils; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serial; + +@Setter +@Getter +@NoArgsConstructor +public class ResultException extends RuntimeException { + @Serial + private static final long serialVersionUID = 1L; + + private String msg; + private String code = "500"; + + public ResultException(String msg) { + super(msg); + this.msg = msg; + } + + public ResultException(String msg, Throwable e) { + super(msg, e); + this.msg = msg; + } + + public ResultException(String msg, String code) { + super(msg); + this.msg = msg; + this.code = code; + } + + public ResultException(EnumUtils.CodeEnum codeEnum){ + super(codeEnum.getMsg()); + this.msg = codeEnum.getMsg(); + this.code = codeEnum.getCode(); + } + + public ResultException(String msg, String code, Throwable e) { + super(msg, e); + this.msg = msg; + this.code = code; + } + + +} \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ServiceException.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ServiceException.java new file mode 100644 index 0000000..7cfe0aa --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/ServiceException.java @@ -0,0 +1,73 @@ +package com.chushang.common.core.exception; + +/** + * 业务异常 + * + * @author ruoyi + */ +public final class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() + { + } + + public ServiceException(String message) + { + this.message = message; + } + + public ServiceException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public String getDetailMessage() + { + return detailMessage; + } + + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } + + public ServiceException setMessage(String message) + { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } +} \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotLoginException.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotLoginException.java new file mode 100644 index 0000000..9887336 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotLoginException.java @@ -0,0 +1,16 @@ +package com.chushang.common.core.exception.auth; + +/** + * 未能通过的登录认证异常 + * + * @author ruoyi + */ +public class NotLoginException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public NotLoginException(String message) + { + super(message); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotPermissionException.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotPermissionException.java new file mode 100644 index 0000000..75d79b0 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotPermissionException.java @@ -0,0 +1,23 @@ +package com.chushang.common.core.exception.auth; + +import org.apache.commons.lang3.StringUtils; + +/** + * 未能通过的权限认证异常 + * + * @author ruoyi + */ +public class NotPermissionException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public NotPermissionException(String permission) + { + super(permission); + } + + public NotPermissionException(String[] permissions) + { + super(StringUtils.join(permissions, ",")); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotRoleException.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotRoleException.java new file mode 100644 index 0000000..5c108d0 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/auth/NotRoleException.java @@ -0,0 +1,23 @@ +package com.chushang.common.core.exception.auth; + +import org.apache.commons.lang3.StringUtils; + +/** + * 未能通过的角色认证异常 + * + * @author ruoyi + */ +public class NotRoleException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public NotRoleException(String role) + { + super(role); + } + + public NotRoleException(String[] roles) + { + super(StringUtils.join(roles, ",")); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/enums/ExceptionEnum.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/enums/ExceptionEnum.java new file mode 100644 index 0000000..a7afb35 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/enums/ExceptionEnum.java @@ -0,0 +1,27 @@ +package com.chushang.common.core.exception.enums; + +import com.chushang.common.core.web.EnumUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * by zhaowenyuan create 2022/1/6 18:00 + */ +@Getter +@AllArgsConstructor +public enum ExceptionEnum implements EnumUtils.CodeEnum{ + RESOURCE_EXIST(3404,"资源不存在"), + APP_RESOURCE_EXIST(3404,"产品资源不存在"), + PLATFORM_RESOURCE_EXIST(3404,"平台资源不存在"), + CONVERT_ERROR(0,"json 转换错误"), + /** + * 数据权限不足 + */ + INSUFFICIENT_DATA_PERMISSION(401,"权限不足, 请联系管理员进行数据授权"), + INSUFFICIENT_DATA_PERMISSION_NULL(401,"数据权限为空, 请联系管理员进行数据授权"), + ; + + private final Integer code; + + private final String msg; +} \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/utils/AssertUtil.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/utils/AssertUtil.java new file mode 100644 index 0000000..4a25bb1 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/exception/utils/AssertUtil.java @@ -0,0 +1,26 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.core.exception.utils; + +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.web.EnumUtils; + +public class AssertUtil { + public AssertUtil() { + } + + public static void invalidate(boolean condition, EnumUtils.CodeEnum msg) { + if (condition) { + throw new ResultException(msg); + } + } + + public static void invalidate(boolean condition, String msg) { + if (condition) { + throw new ResultException(msg); + } + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/handler/GlobalExceptionHandler.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..7708377 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/handler/GlobalExceptionHandler.java @@ -0,0 +1,144 @@ +package com.chushang.common.core.handler; + +import cn.hutool.http.HttpStatus; +import com.chushang.common.core.exception.CheckedException; +import com.chushang.common.core.exception.InnerAuthException; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.exception.ServiceException; +import com.chushang.common.core.exception.auth.NotPermissionException; +import com.chushang.common.core.exception.auth.NotRoleException; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.web.AjaxResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; + +/** + * 全局异常处理器 + * + * @author ruoyi + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限码异常 + */ + @ExceptionHandler(NotPermissionException.class) + public AjaxResult handleNotPermissionException(NotPermissionException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权"); + } + + /** + * 角色权限异常 + */ + @ExceptionHandler(NotRoleException.class) + public AjaxResult handleNotRoleException(NotRoleException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e) + { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage(), e); + String message = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 内部认证异常 + */ + @ExceptionHandler(InnerAuthException.class) + public AjaxResult handleInnerAuthException(InnerAuthException e) + { + return AjaxResult.error(e.getMessage()); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(ResultException.class) + public AjaxResult handlerResultException(ResultException e, HttpServletRequest request){ + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getCode(), e.getMessage()); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(CheckedException.class) + public AjaxResult handlerCheckedException(CheckedException e, HttpServletRequest request){ + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JacksonUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JacksonUtils.java new file mode 100644 index 0000000..40a714b --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JacksonUtils.java @@ -0,0 +1,49 @@ +package com.chushang.common.core.jackson; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; + +@Slf4j +public class JacksonUtils { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @SneakyThrows + public static String toJSONString(Object data) { + if (null == data) { + return ""; + } + return mapper.writeValueAsString(data); + } + + @SneakyThrows + public static T json2Bean(String jsonData, Class beanType) { + if (null == jsonData) { + return null; + } + return mapper.readValue(jsonData, beanType); + } + + @SneakyThrows + public static List json2List(String jsonData, Class beanType) { + if (null == jsonData) { + return List.of(); + } + JavaType javaType = mapper.getTypeFactory().constructParametricType(List.class, beanType); + return mapper.readValue(jsonData, javaType); + } + + @SneakyThrows + public static Map json2Map(String jsonData, Class keyType, Class valueType) { + if (null == jsonData) { + return Map.of(); + } + JavaType javaType = mapper.getTypeFactory().constructMapType(Map.class, keyType, valueType); + return mapper.readValue(jsonData, javaType); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JavaTimeModule.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JavaTimeModule.java new file mode 100644 index 0000000..d570fc8 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/jackson/JavaTimeModule.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.chushang.common.core.jackson; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.PackageVersion; +import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class JavaTimeModule extends SimpleModule { + + public JavaTimeModule() { + super(PackageVersion.VERSION); + + // ======================= 时间序列化规则 =============================== + // yyyy-MM-dd HH:mm:ss + this.addSerializer(LocalDateTime.class, + new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + // yyyy-MM-dd + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)); + // HH:mm:ss + this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME)); + // Instant 类型序列化 + this.addSerializer(Instant.class, InstantSerializer.INSTANCE); + + // ======================= 时间反序列化规则 ============================== + // yyyy-MM-dd HH:mm:ss + this.addDeserializer(LocalDateTime.class, + new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + // yyyy-MM-dd + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)); + // HH:mm:ss + this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME)); + // Instant 反序列化 + this.addDeserializer(Instant.class, InstantDeserializer.INSTANT); + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/CharsetKit.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/CharsetKit.java new file mode 100644 index 0000000..a02b56c --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/CharsetKit.java @@ -0,0 +1,87 @@ +package com.chushang.common.core.text; + +import com.chushang.common.core.util.StringUtils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * 字符集工具类 + * + * @author ruoyi + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/Convert.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/Convert.java new file mode 100644 index 0000000..e08ab59 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/Convert.java @@ -0,0 +1,1017 @@ +package com.chushang.common.core.text; + +import com.chushang.common.core.util.StringUtils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * 类型转换器 + * + * @author ruoyi + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + if (null == value) return null; + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + return toStrArray(",", str); + } + + public static List toList(String str){ + return Arrays.asList(toStrArray(str)); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + return true; + case "false": + return false; + case "yes": + return true; + case "ok": + return true; + case "no": + return false; + case "1": + return true; + case "0": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return new BigDecimal((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[] || obj instanceof Byte[]) + { + if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else + { + Byte[] bytes = (Byte[]) obj; + int length = bytes.length; + byte[] dest = new byte[length]; + for (int i = 0; i < length; i++) + { + dest[i] = bytes[i]; + } + return str(dest, charset); + } + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char c[] = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char c[] = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + String returnString = new String(c); + + return returnString; + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + s += (digit[(int) (Math.floor(n * 10 * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/StrFormatter.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/StrFormatter.java new file mode 100644 index 0000000..05ef30c --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/StrFormatter.java @@ -0,0 +1,87 @@ +package com.chushang.common.core.text; + +import com.chushang.common.core.util.StringUtils; + +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/ToolUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/ToolUtils.java new file mode 100644 index 0000000..7fbd7e1 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/text/ToolUtils.java @@ -0,0 +1,319 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.core.text; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.alibaba.fastjson2.JSONArray; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ToolUtils { + private static final Logger log = LoggerFactory.getLogger(ToolUtils.class); + + public ToolUtils() { + } + + public static boolean isBlank(Object obj) { + if (obj != null && !"".equals(obj) && !StringUtils.isBlank(obj.toString())) { + if (obj instanceof JSONArray) { + return ((JSONArray) obj).isEmpty(); + } else if (obj instanceof CharSequence) { + return ((CharSequence) obj).isEmpty(); + } else if (obj instanceof Collection) { + return ((Collection)obj).isEmpty(); + } else if (obj instanceof Map) { + return ((Map)obj).isEmpty(); + } else if (!(obj instanceof Object[] object)) { + return false; + } else { + if (object.length == 0) { + return true; + } else { + boolean empty = true; + for (Object o : object) { + if (!isBlank(o)) { + empty = false; + break; + } + } + return empty; + } + } + } else { + return true; + } + } + + public static String dencodeURI(String beforeStr) { + return beforeStr != null ? URLDecoder.decode(beforeStr, StandardCharsets.UTF_8) : null; + } + + public static String dencodeURI(String beforeStr, String charset) { + try { + return beforeStr != null ? URLDecoder.decode(beforeStr, charset) : null; + } catch (UnsupportedEncodingException var3) { + log.error("dencode Exception", var3); + var3.printStackTrace(); + return ""; + } + } + + public static String encodeString(String beforeStr, String charset) { + try { + return beforeStr != null ? new String(beforeStr.getBytes(StandardCharsets.ISO_8859_1), charset) : null; + } catch (UnsupportedEncodingException var3) { + log.error(" ISO-8859-1 to Byte convert to " + charset + " exception", var3); + var3.printStackTrace(); + return ""; + } + } + + public static String encodeURI(String beforeStr) { + return beforeStr != null ? URLEncoder.encode(beforeStr, StandardCharsets.UTF_8) : null; + } + + public static String encodeURI(String beforeStr, String charset) { + try { + return beforeStr != null ? URLEncoder.encode(beforeStr, charset) : null; + } catch (UnsupportedEncodingException var3) { + log.error("URLEncoder encode by " + charset + " Exception", var3); + var3.printStackTrace(); + return ""; + } + } + + public static boolean isCharacter(String str) { + boolean temp = false; + temp = str.matches("[\\u4e00-\\u9fa5]"); + return temp; + } + + public static boolean isDate(String str) { + String eL = "^((\\d{2}(([02468][048])|([13579][26]))[\\-/\\s]?((((0?[13578])|(1[02]))[\\-/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-/\\s]?((0?[1-9])|([1-2][0-9])))))|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-/\\s]?((((0?[13578])|(1[02]))[\\-/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-/\\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))"; + Pattern pattern = Pattern.compile(eL); + return pattern.matcher(str).matches(); + } + + public static boolean isEmpty(List list) { + return isNull(list) || list.isEmpty(); + } + + public static boolean isEmpty(Object[] obj) { + return isNull(obj) || obj.length == 0; + } + + public static boolean isEmpty(String str) { + return StringUtils.isEmpty(str); + } + + public static boolean isNull(Object object) { + return object == null; + } + + public static boolean isLetter(String inputString) { + return inputString != null && inputString.matches("[a-zA-Z]+"); + } + + public static boolean isMail(String smail) { + Pattern pattern = Pattern.compile("[\\w.\\-]+@([\\w\\-]+\\.)+[\\w\\-]+", 2); + return pattern.matcher(smail).matches(); + } + + public static boolean isMobileNumber(String num) { + String regex = "^((13[0-9])|(14[5,79])|(15([0-3]|[5-9]))|(166)|(17[0,135678])|(18[0-9])|(19[0-9]))\\d{8}$"; + if (num.length() != 11) { + return false; + } else { + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(num); + return m.matches(); + } + } + + public static boolean isNotEmpty(String str) { + return str != null && !str.trim().isEmpty(); + } + + public static boolean isNumber(String inputString) { + return inputString != null && inputString.matches("[0-9]+"); + } + + public static double div(double v1, double v2, int scale) { + if (scale < 0) { + throw new IllegalArgumentException("The scale must be a positive integer or zero"); + } else { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.divide(b2, scale, 4).doubleValue(); + } + } + + public static boolean isZipCodeNumber(String num) { + return Pattern.matches("\\d{6}", num); + } + + public static String random(int n) { + StringBuilder s = new StringBuilder(); + + for(int i = 0; i < n; ++i) { + int a = (int)(Math.random() * 10.0); + s.append(a); + } + + return s.toString(); + } + + public static String randomChar(int n) { + StringBuilder str = new StringBuilder(); + char[] cha = new char[52]; + + int i; + for(i = 0; i < 26; ++i) { + cha[i] = (char)(65 + i); + } + + while(i < 52) { + cha[i] = (char)(71 + i); + ++i; + } + + Random random = new Random(); + + for(i = 0; i < n; ++i) { + int rand = random.nextInt(52); + str.append(cha[rand]); + } + + return str.toString(); + } + + public static String randomString(int n) { + String str = ""; + char[] cha = new char[62]; + + int i; + for(i = 0; i < 26; ++i) { + cha[i] = (char)(65 + i); + } + + while(i < 52) { + cha[i] = (char)(71 + i); + ++i; + } + + while(i < 62) { + cha[i] = (char)(-4 + i); + ++i; + } + + Random random = new Random(); + + for(i = 0; i < n; ++i) { + int rand = random.nextInt(62); + str = str + cha[rand]; + } + + return str; + } + + public static Serializable[] split(String str, String expression) { + if (str != null && str != "") { + StringTokenizer a = new StringTokenizer(str, expression); + int i = 0; + Serializable[] strs = new Serializable[a.countTokens()]; + if (a.countTokens() > 0) { + try { + while(a.hasMoreTokens()) { + strs[i] = a.nextToken(); + ++i; + } + } catch (ArrayIndexOutOfBoundsException var6) { + ArrayIndexOutOfBoundsException e = var6; + log.error("数组越界", e); + e.printStackTrace(); + } + } + + return strs; + } else { + Serializable[] o = new Serializable[0]; + return o; + } + } + + public static String arrayToStr(Serializable[] array, String insertString) { + String strs = ""; + if (array != null) { + for(int i = 0; i < array.length; ++i) { + if (i < array.length - 1) { + strs = strs + array[i] + insertString; + } else { + strs = strs + array[i]; + } + } + } + + return strs; + } + + public static String[] strSplit(String str, String expression) { + if (str != null && str != "") { + StringTokenizer a = new StringTokenizer(str, expression); + int i = 0; + String[] strs = new String[a.countTokens()]; + if (a.countTokens() > 0) { + try { + while(a.hasMoreTokens()) { + String token = a.nextToken(); + if (!isEmpty(token) && !isEmpty(token = token.trim())) { + strs[i] = token; + ++i; + } + } + } catch (ArrayIndexOutOfBoundsException var6) { + ArrayIndexOutOfBoundsException e = var6; + log.error("数组越界", e); + e.printStackTrace(); + } + } + + return strs; + } else { + String[] o = new String[0]; + return o; + } + } + + public static String unicodeEncoding(String str) { + StringBuffer unicodeBytes = new StringBuffer(); + + for(int byteIndex = 0; byteIndex < str.length(); ++byteIndex) { + String hexB = Integer.toHexString(str.charAt(byteIndex)); + unicodeBytes.append("\\u"); + if (hexB.length() <= 2) { + unicodeBytes.append("00"); + } + + unicodeBytes.append(hexB); + } + + return unicodeBytes.toString(); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ClassUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ClassUtils.java new file mode 100644 index 0000000..f87921c --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ClassUtils.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.util; + +import cn.hutool.core.collection.CollectionUtil; +import com.chushang.common.core.constant.Constants; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +/** + * 类工具类 + * + * @author L.cm + */ +@Slf4j +@UtilityClass +public class ClassUtils extends org.springframework.util.ClassUtils { + + private final ParameterNameDiscoverer PARAMETERNAMEDISCOVERER = new DefaultParameterNameDiscoverer(); + + /** + * 获取方法参数信息 + * @param constructor 构造器 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public MethodParameter getMethodParameter(Constructor constructor, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER); + return methodParameter; + } + + /** + * 获取方法参数信息 + * @param method 方法 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public MethodParameter getMethodParameter(Method method, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER); + return methodParameter; + } + + /** + * 获取Annotation + * @param method Method + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public A getAnnotation(Method method, Class annotationType) { + Class targetClass = method.getDeclaringClass(); + // The method may be on an interface, but we need attributes from the target + // class. + // If the target class is null, the method will be unchanged. + Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); + // If we are dealing with method with generic parameters, find the original + // method. + specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + // 先找方法,再找方法上的类 + A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType); + + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType); + } + + /** + * 获取Annotation + * @param handlerMethod HandlerMethod + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + // 先找方法,再找方法上的类 + A annotation = handlerMethod.getMethodAnnotation(annotationType); + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + Class beanType = handlerMethod.getBeanType(); + return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType); + } + public boolean objCheckNonNull(Object object) + { + if (null == object){ + return false; + } + // 默认忽略 序列化 + return objCheckNonNull(object, Set.of(Constants.SERIAL_VERSION_STR)); + } + + /** + * 只要有一个 非null 值即为 true + */ + public boolean objCheckNonNull(Object object, Set ignoreFieldNames) + { + Class clazz = object.getClass(); + Field[] fields = clazz.getDeclaredFields(); + boolean flag = false; + for(Field field : fields){ + field.setAccessible(true); + Object fieldValue = null; + try { + String fieldName = field.getName(); + if (CollectionUtil.isNotEmpty(ignoreFieldNames)){ + // 要求 fieldName 必须一致, 大小写不同, 视为不同 + if (ignoreFieldNames.contains(fieldName)) + continue; + } + fieldValue = field.get(object); + Type fieldType =field.getGenericType(); + if (log.isDebugEnabled()){ + log.debug("类名: {}, 属性类型: {},属性名: {}, 属性值: {}",clazz.getName(), fieldType, fieldName, fieldValue); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + if(fieldValue != null){ + flag = true; + break; + } + } + return flag; + } + + public boolean objCheckAllNotNull(Object object){ + if (null == object) return false; + return objCheckAllNotNull(object, Set.of(Constants.SERIAL_VERSION_STR)); + } + /** + * + */ + public boolean objCheckAllNotNull(Object object, Set ignoreFieldNames){ + // 默认其为true + Set flagSet = new HashSet<>(); + flagSet.add(true); + Class clazz = object.getClass(); + Field[] fields = clazz.getDeclaredFields(); + for(Field field : fields){ + field.setAccessible(true); + Object fieldValue = null; + try { + String fieldName = field.getName(); + if (CollectionUtil.isNotEmpty(ignoreFieldNames)){ + // 要求 fieldName 必须一致, 大小写不同, 视为不同 + if (ignoreFieldNames.contains(fieldName)) + continue; + } + fieldValue = field.get(object); + Type fieldType =field.getGenericType(); + if (log.isDebugEnabled()){ + log.debug("类名: {}, 属性类型: {},属性名: {}, 属性值: {}",clazz.getName(), fieldType, fieldName, fieldValue); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + flagSet.add(fieldValue != null); + } + // 为1 时, 代表没有 false --> 也就是没有 null 值, 否则就是存在null 值 + return flagSet.size() == 1; + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/CommonParam.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/CommonParam.java new file mode 100644 index 0000000..aa3ac45 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/CommonParam.java @@ -0,0 +1,46 @@ +package com.chushang.common.core.util; + +import lombok.Data; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * by zhaowenyuan create 2021/11/5 14:53 + * 公共 请求 query + */ +@Data +public class CommonParam implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 当前页码 + */ + private Integer page = 1; + /** + * 页面大小 + */ + private Integer limit = 10; + /** + * 排序字段: 排序方式 -- 为 null 时, 没有排序 + */ + private String sortStr; + /** + * 额外分组选项 --> 比如需要根据国家, 或者 pid 进行分组 + */ + private String groupStr; + + + private Map sqlParam; + + public Map getSqlParam() + { + if (sqlParam == null) + { + sqlParam = new HashMap<>(); + } + return sqlParam; + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ConcurrencyUtilTest.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ConcurrencyUtilTest.java new file mode 100644 index 0000000..a210c5e --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ConcurrencyUtilTest.java @@ -0,0 +1,63 @@ +package com.chushang.common.core.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.*; + +/** + * @author by zhaowenyuan create 2022/7/18 10:14 + * 并发测试 + */ +@Slf4j +public class ConcurrencyUtilTest { + // 请求总数 + public static int clientTotal = 10000000; + + // 同时并发执行的线程数 + public static int threadTotal = 1000; + + public static void main(String[] args) { + Map map = new ConcurrentHashMap<>(); + try { + insert(map); + } catch (InterruptedException e) { + e.printStackTrace(); + } + log.info("{}",map); + log.info("{}",map.values().stream().max(Integer::compareTo).orElse(0)); + log.info("{}", map.values().stream().count()); + } + + public static void insert(Map map) throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(threadTotal); + //信号量,此处用于控制并发的线程数 + final Semaphore semaphore = new Semaphore(threadTotal); + //闭锁,可实现计数器递减 + final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); + for (int i = 0; i < clientTotal; i++) { + executorService.execute(() -> { + try { + //执行此方法用于获取执行许可,当总计未释放的许可数不超过200时, + //允许通行,否则线程阻塞等待,直到获取到许可。 + semaphore.acquire(); + +// /** +// * 放置具体的 待测试并发逻辑 +// */ +// String numId = IdUtils.getTimeId(); +// map.merge(numId, 1, Integer::sum); + + //释放许可 + semaphore.release(); + } catch (Exception e) { + e.printStackTrace(); + } + countDownLatch.countDown(); + }); + } + //线程阻塞,直到闭锁值为0时,阻塞才释放,继续往下执行 + countDownLatch.await(); + executorService.shutdown(); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/DateUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/DateUtils.java new file mode 100644 index 0000000..2a6f1b7 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/DateUtils.java @@ -0,0 +1,327 @@ +package com.chushang.common.core.util; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.util.ObjectUtil; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; + +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.WeekFields; +import java.util.*; + +/** + * by zhaowenyuan create 2021/10/26 14:52 + * 时间工具类 + */ +public class DateUtils { + private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN); + public final static String DEFAULT_TIME_ZONE = "GMT"; + + /** + * 时间戳 + * @author ant + * @return long + */ + public static long getZoneOfMillis(){ + // 获取 Utc 的 localDateTime 并且将其转为时间戳 + return getZoneOfMillis(LocalDateTime.now(ZoneOffset.UTC)); + } + + public static long getZoneOfMillis(String dateTime){ + Instant instant = Instant.parse(dateTime); + return instant.toEpochMilli(); + } + + public static long getSecondAtStartOfDay(String date){ + return getMillisAtStartOfDay(date) / 1000; + } + + public static long getMillisAtStartOfDay(String date){ + return getMillisAtStartOfDay(parseDate(date)); + } + + public static long getSecondAtStartOfDay(LocalDate date){ + return getMillisAtStartOfDay(date) / 1000; + } + + public static long getMillisAtStartOfDay(LocalDate date){ + return getZoneOfMillis(date.atStartOfDay()); + } + + public static long getSecondAtEndOfDay(String date){ + return getMillisAtEndOfDay(date) / 1000; + } + + public static long getSecondAtEndOfDay(LocalDate date){ + return getMillisAtEndOfDay(date) / 1000; + } + public static long getMillisAtEndOfDay(String date){ + return getMillisAtEndOfDay(parseDate(date)); + } + + public static long getMillisAtEndOfDay(LocalDate date){ + return getZoneOfMillis(date.atTime(23,59,59)); + } + + public static long getZoneOfMillis(LocalDateTime localDateTime){ + return localDateTime.atZone(ZoneOffset.UTC).toInstant().toEpochMilli(); + } + + public static long getZoneOfMillis(LocalDateTime localDateTime, ZoneOffset zoneOffset){ + return localDateTime.atZone(zoneOffset).toInstant().toEpochMilli(); + } + + public static long getMillisAtStartOfDay(LocalDate date, ZoneOffset zoneOffset){ + return getZoneOfMillis(date.atStartOfDay(), zoneOffset); + } + + public static long getZoneOfSecond() { + return getZoneOfMillis() / 1000; + } + + public static long getZoneOfSecond(LocalDateTime localDateTime) { + return getZoneOfMillis(localDateTime) / 1000; + } + /** + * 获取 utc+0 的时间字符串 + */ + public static String getUtcTime(){ + return format(ZonedDateTime.now(ZoneOffset.UTC)); + } + + /** + * 格式化 + */ + public static String format(ZonedDateTime zonedDateTime){ + return format(zonedDateTime, dtf); + } + /** + * 格式化 + */ + public static String format(ZonedDateTime zonedDateTime, DateTimeFormatter formatter){ + return zonedDateTime.format(formatter); + } + + /** + * 格式化 + */ + public static String format(LocalDateTime localDateTime, DateTimeFormatter formatter){ + return localDateTime.format(formatter); + } + + /** + * 格式化 + */ + public static String format(LocalDateTime localDateTime){ + return format(localDateTime, dtf); + } + + + /** 时间格式(yyyy-MM-dd) */ + public final static String DATE_PATTERN = "yyyy-MM-dd"; + + public static LocalDate toLocalDate(long secondTimestamp){ + return Instant.ofEpochSecond(secondTimestamp).atZone(ZoneId.systemDefault()).toLocalDate(); + } + + public static LocalDate parseDate(String date) + { + return parseDate(date,DATE_PATTERN); + } + + public static String format(LocalDate date){ + return format(date,DATE_PATTERN); + } + + public static String format(LocalDate date,String pattern){ + if (ObjectUtil.isNull(date)){ + return null; + } + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return date.format(dtf); + } + + public static LocalDate parseDate(String date, String pattern) + { + if (StringUtils.isBlank(date)){ + return null; + } + return LocalDate.parse(date,DateTimeFormatter.ofPattern(pattern)); + } + + /** + * 日期格式化 日期格式为:yyyy-MM-dd + * @param date 日期 + * @return 返回yyyy-MM-dd格式日期 + */ + public static String format(Date date) { + return format(date, DATE_PATTERN); + } + + /** + * 日期格式化 日期格式为:yyyy-MM-dd + * @param date 日期 + * @param pattern 格式,如:DatePattern.NORM_DATETIME_PATTERN + * @return 返回yyyy-MM-dd格式日期 + */ + public static String format(Date date, String pattern) { + if(date != null){ + SimpleDateFormat df = new SimpleDateFormat(pattern); + return df.format(date); + } + return null; + } + + public static String format(ZonedDateTime zonedDateTime,String pattern){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return zonedDateTime.format(formatter); + } + + /** + * 计算日期的 区间 + * @param startDateStr 开始时间 + * @param endDateStr 结束时间 + */ + public static Set dateIntervalCalculation(String startDateStr, String endDateStr) + { + Clock clock = Clock.system(ZoneId.of(DateUtils.DEFAULT_TIME_ZONE)); + // 开始日期 + ZonedDateTime startDateTime = parseDate(startDateStr).atStartOfDay().atZone(clock.getZone()); + // 结束日期 + ZonedDateTime endDateTime = parseDate(endDateStr).atStartOfDay().atZone(clock.getZone()); + Set timeSet = new HashSet<>(); + for (ZonedDateTime start = startDateTime; endDateTime.isAfter(start); start = start.plusDays(1L)){ + timeSet.add(format(start, DateUtils.DATE_PATTERN)); + } + // 最后需要添加一下 结束日期 + timeSet.add(format(endDateTime, DateUtils.DATE_PATTERN)); + return timeSet; + } + + @SneakyThrows + public static Set dateIntervalCalculationByMonth(String startDateStr, String endDateStr) + { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_MONTH_PATTERN); + Date startDate = simpleDateFormat.parse(startDateStr); + Date endDate = simpleDateFormat.parse(endDateStr); + Calendar min = Calendar.getInstance(); + min.setTime(startDate); + Set timeSet = new HashSet<>(); + while (!min.getTime().after(endDate)){ + timeSet.add(simpleDateFormat.format(min.getTime())); + min.add(Calendar.MONTH, 1); + } + return timeSet; + } + + /** + * 根据传入的日期判断 日期属于年份的第几周 + * 如果 week 小于10 , 则需要进行补0 + * date 应该是具体的实际日期 -- 即不进行获取周一,周末 + * @param date 某个指定日期 + */ + public static String weekOfYear(LocalDate date) + { + WeekFields weekFields = WeekFields.of(DayOfWeek.MONDAY,4); + int weekYear = date.getYear(); + int week = date.get(weekFields.weekOfWeekBasedYear()); + if (week < 10) + return weekYear + "0" + week; + return weekYear + "" + week; + } + + public static String weekOfYearName(LocalDate date) + { + WeekFields weekFields = WeekFields.of(DayOfWeek.MONDAY,4); + int weekYear = date.getYear(); + int week = date.get(weekFields.weekOfWeekBasedYear()); + return weekYear + "年第" + week + "周"; + } + + public static String quarterOfYear(String date){ + String[] startSplit = date.split("-"); + // 开始季度 + int quarter = Integer.parseInt(startSplit[1]); + // 开始年份 + int year = Integer.parseInt(startSplit[0]); + return year + "年第" + quarter + "季度"; + } + + + public static String formatYearMonth(String date){ + return parseDate(date).format(DateTimeFormatter.ofPattern("yyyy_MM")); + } + + public static String formatYearMonthDay(String date){ + return parseDate(date).format(DateTimeFormatter.ofPattern("yyyy_MM_dd")); + } + public static String formatYearMonthDay(LocalDate date){ + return date.format(DateTimeFormatter.ofPattern("yyyy_MM_dd")); + } + + public final static String ISO_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + public final static String DATE_PATTERN_2 = "yyyyMMdd"; + public static String isoFormat(LocalDateTime dateTime) + { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(ISO_DATE_TIME_PATTERN); + return dateTime.format(dtf); + } + + public static Long getSecond(String offsetId) { + return LocalDateTime.now().toEpochSecond(ZoneOffset.of(offsetId)); + } + + + public static Date getThisWeekThursday(String day){ + return getWeekDate(day); + } + + /** + * 获取本周4 + * + * @param day 日期 + */ + private static Date getWeekDate(String day) { + Date date = stringToDate(day, DATE_PATTERN); + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + // 获得当前日期是一个星期的第几天 + int dayWeek = cal.get(Calendar.DAY_OF_WEEK); + if (1 == dayWeek) { + cal.add(Calendar.DAY_OF_MONTH, -1); + } + // 设置一个星期的第一天,按中国的习惯一个星期的第一天是星期一 + cal.setFirstDayOfWeek(Calendar.MONDAY); + // 获得当前日期是一个星期的第几天 + int week = cal.get(Calendar.DAY_OF_WEEK); + // 根据日历的规则,给当前日期减去星期几与一个星期第一天的差值 + cal.add(Calendar.DATE, cal.getFirstDayOfWeek() - (week - 3)); + return cal.getTime(); + } + + /** + * 字符串转换成日期 + * @param strDate 日期字符串 + * @param pattern 日期的格式,如:DatePattern.NORM_DATETIME_PATTERN + */ + public static Date stringToDate(String strDate, String pattern) { + LocalDate localDate = parseDate(strDate, pattern); + ZoneId zone = ZoneId.systemDefault(); + assert localDate != null; + Instant instant = localDate.atStartOfDay().atZone(zone).toInstant(); + return Date.from(instant); + } + + + public static boolean judgeEquMonth(String startDateStr, String endDateStr) { + if (StringUtils.isEmpty(startDateStr) || StringUtils.isEmpty(endDateStr)){ + return false; + } + // 均设置为1号, 进行比较 + LocalDate startDate = parseDate(startDateStr).withDayOfMonth(1); + LocalDate endDate = parseDate(endDateStr).withDayOfMonth(1); + return startDate.equals(endDate); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/FileUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/FileUtils.java new file mode 100644 index 0000000..75f09a5 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/FileUtils.java @@ -0,0 +1,256 @@ +package com.chushang.common.core.util; + +import cn.hutool.core.io.FileUtil; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class FileUtils extends FileUtil { + private static final Logger log = LoggerFactory.getLogger(FileUtils.class); + + @SneakyThrows + public static void createFile(InputStream inputStream, String filename) { + File tmp = new File(filename); + OutputStream os = Files.newOutputStream(tmp.toPath()); + try (inputStream) { + int bytesRead; + byte[] buffer = new byte[8192]; + while ((bytesRead = inputStream.read(buffer, 0, 8192)) != -1) { + os.write(buffer, 0, bytesRead); + } + } + } + + private static final char[] Digit = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + public static void writeBytes(String filePath, OutputStream os) throws IOException { + FileInputStream fis = null; + + try { + File file = new File(filePath); + if (!file.exists()) { + throw new FileNotFoundException(filePath); + } + + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + + int length; + while ((length = fis.read(b)) > 0) { + os.write(b, 0, length); + } + } catch (IOException var9) { + IOException e = var9; + throw e; + } finally { + if (os != null) { + os.close(); + } + + if (fis != null) { + fis.close(); + } + + } + + } + + public static boolean deleteFile(String filePath) { + boolean flag = false; + File file = new File(filePath); + if (file.isFile() && file.exists()) { + file.delete(); + flag = true; + } + + return flag; + } + + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException { + String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) { + filename = URLEncoder.encode(filename, StandardCharsets.UTF_8); + filename = filename.replace("+", " "); + } else if (agent.contains("Firefox")) { + filename = new String(fileName.getBytes(), "ISO8859-1"); + } else if (agent.contains("Chrome")) { + filename = URLEncoder.encode(filename, StandardCharsets.UTF_8); + } else { + filename = URLEncoder.encode(filename, StandardCharsets.UTF_8); + } + + return filename; + } + + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException { + String percentEncodedFileName = percentEncode(realFileName); + response.setHeader("Content-disposition", "attachment; filename=" + percentEncodedFileName + ";" + "filename*=" + "utf-8''" + percentEncodedFileName); + } + + public static String percentEncode(String s) throws UnsupportedEncodingException { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8); + return encode.replaceAll("\\+", "%20"); + } + + public static String getFileExtendName(byte[] photoByte) { + String strFileExtendName = "jpg"; + if (photoByte[0] == 71 && photoByte[1] == 73 && photoByte[2] == 70 && photoByte[3] == 56 && (photoByte[4] == 55 || photoByte[4] == 57) && photoByte[5] == 97) { + strFileExtendName = "gif"; + } else if (photoByte[6] == 74 && photoByte[7] == 70 && photoByte[8] == 73 && photoByte[9] == 70) { + strFileExtendName = "jpg"; + } else if (photoByte[0] == 66 && photoByte[1] == 77) { + strFileExtendName = "bmp"; + } else if (photoByte[1] == 80 && photoByte[2] == 78 && photoByte[3] == 71) { + strFileExtendName = "png"; + } + + return strFileExtendName; + } + + public static String getName(String fileName) { + if (fileName == null) { + return null; + } else { + int lastUnixPos = fileName.lastIndexOf(47); + int lastWindowsPos = fileName.lastIndexOf(92); + int index = Math.max(lastUnixPos, lastWindowsPos); + return fileName.substring(index + 1); + } + } + + public static String getNameNotSuffix(String fileName) { + String baseName = getName(fileName); + if (baseName != null && !baseName.trim().isEmpty()) { + int index = baseName.lastIndexOf("."); + return baseName.substring(0, index); + } else { + return null; + } + } + + public static String getExtension(String fileName) { + String baseName = getName(fileName); + if (baseName != null && !baseName.trim().isEmpty()) { + int index = baseName.lastIndexOf("."); + return baseName.substring(index + 1); + } else { + return null; + } + } + + public static String getMd5(File f) { + try (FileInputStream fis = new FileInputStream(f)){ + return getMd5(fis); + }catch (Exception e){ + e.printStackTrace(); + } + return ""; + } + + public static String getMd5(InputStream is) throws IOException, NoSuchAlgorithmException { + MessageDigest mdInst = MessageDigest.getInstance("MD5"); + byte[] buffer = new byte[1024]; + while (is.read(buffer) != -1) { + mdInst.update(buffer); + } + return binaryToHexString(mdInst.digest()); + } + + private static String binaryToHexString(byte[] result) { + StringBuilder digestHexStr = new StringBuilder(); + + for (int i = 0; i < 16; ++i) { + char[] ob = new char[]{Digit[result[i] >>> 4 & 15], Digit[result[i] & 15]}; + String s = new String(ob); + digestHexStr.append(s); + } + + return digestHexStr.toString(); + } + + public static boolean isText(File file) { + boolean isText = true; + try (FileInputStream fin = new FileInputStream(file)){ + long len = file.length(); + for (int j = 0; j < (int) len; ++j) { + int t = fin.read(); + if (t < 32 && t != 9 && t != 10 && t != 13) { + isText = false; + break; + } + } + }catch (Exception e){ + log.error("isText", e); + } + return isText; + } + + public static String getFileCharset(File sourceFile) { + String charset = "GBK"; + byte[] first3Bytes = new byte[3]; + String var7; + try (FileInputStream fis = new FileInputStream(sourceFile); + BufferedInputStream bis = new BufferedInputStream(fis)){ + boolean checked = false; + bis.mark(0); + int read = bis.read(first3Bytes, 0, 3); + if (read != -1) { + if (first3Bytes[0] == -1 && first3Bytes[1] == -2) { + charset = "UTF-16LE"; + checked = true; + } else if (first3Bytes[0] == -2 && first3Bytes[1] == -1) { + charset = "UTF-16BE"; + checked = true; + } else if (first3Bytes[0] == -17 && first3Bytes[1] == -69 && first3Bytes[2] == -65) { + charset = "UTF-8"; + checked = true; + } + bis.reset(); + if (!checked) { + label248: + do { + do { + if ((read = bis.read()) == -1 || read >= 240 || 128 <= read && read <= 191) { + break label248; + } + + if (192 <= read && read <= 223) { + read = bis.read(); + continue label248; + } + } while(224 > read || read > 239); + + read = bis.read(); + if (128 <= read && read <= 191) { + read = bis.read(); + if (128 <= read && read <= 191) { + charset = "UTF-8"; + } + } + break; + } while(128 <= read && read <= 191); + } + + bis.close(); + return charset; + } + + var7 = charset; + } catch (Exception e) { + e.printStackTrace(); + return charset; + } + return var7; + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/HexByte.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/HexByte.java new file mode 100644 index 0000000..6c1b7bb --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/HexByte.java @@ -0,0 +1,47 @@ +package com.chushang.common.core.util; + +public final class HexByte +{ + private HexByte() + { + + } + + /** + * 将二进制转换成16进制 + * @param buf + * @return + */ + public static String parseByte2HexStr(byte[] buf) + { + StringBuilder sb = new StringBuilder(); + for (byte b : buf) { + String hex = Integer.toHexString(b & 0xFF); + if (hex.length() == 1) { + hex = '0' + hex; + } + sb.append(hex.toUpperCase()); + } + return sb.toString(); + } + + /** + * 将16进制转换为二进制 + * @param hexStr + * @return + */ + public static byte[] parseHexStr2Byte(String hexStr) + { + if (hexStr.length() < 1) + return null; + byte[] result = new byte[hexStr.length() / 2]; + for (int i = 0; i < hexStr.length() / 2; i++) + { + int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16); + int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16); + result[i] = (byte) (high * 16 + low); + } + return result; + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IPUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IPUtils.java new file mode 100644 index 0000000..747b643 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IPUtils.java @@ -0,0 +1,321 @@ +package com.chushang.common.core.util;// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; + +import com.alibaba.fastjson2.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; + +public class IPUtils { + private static final Logger log = LoggerFactory.getLogger(IPUtils.class); + + public IPUtils() { + } + + public static String clientIp(HttpServletRequest request) { + String realIps = clientIps(request); + return realIps.contains(",") ? realIps.split(",")[0] : realIps; + } + + public static String clientIps(HttpServletRequest request) + { + String ip = request.getHeader("X-Forwarded-For"); + log.debug("X-Forwarded-For ==>" + ip); + if (checkIp(ip)) { + ip = request.getHeader("X-Real-IP"); + log.debug("X-Real-IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + log.debug("Proxy-Client-IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + log.debug("WL-Proxy-Client-IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + log.debug("HTTP_CLIENT_IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + log.debug("HTTP_X_FORWARDED_FOR ==>" + ip); + } + + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + log.debug("request.getRemoteAddr() ==>" + ip); + } + + log.info("RealRemoteIP ==>" + ip); + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; + } + + public static String clientIp(ServerHttpRequest request) + { + String realIps = clientIps(request); + return realIps.contains(",") ? realIps.split(",")[0] : realIps; + } + + public static String clientIps(ServerHttpRequest request) + { + HttpHeaders headers = request.getHeaders(); + String ip = headers.getFirst("X-Forwarded-For"); + // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 + log.debug("X-Forwarded-For ==>" + ip); + if (checkIp(ip)) { + ip = headers.getFirst("X-Real-IP"); + log.debug("X-Real-IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = headers.getFirst("Proxy-Client-IP"); + log.debug("Proxy-Client-IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = headers.getFirst("WL-Proxy-Client-IP"); + log.debug("WL-Proxy-Client-IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = headers.getFirst("HTTP_CLIENT_IP"); + log.debug("HTTP_CLIENT_IP ==>" + ip); + } + + if (checkIp(ip)) { + ip = headers.getFirst("HTTP_X_FORWARDED_FOR"); + log.debug("HTTP_X_FORWARDED_FOR ==>" + ip); + } + + if (checkIp(ip)) { + ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress(); + log.debug("request.getRemoteAddr() ==>" + ip); + } + + log.info("RealRemoteIP ==>" + ip); + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; + } + + private static boolean checkIp(String ip) { + return null == ip || ip.trim().isEmpty() || "unknown".equalsIgnoreCase(ip); + } + + public static boolean isInvalid(String ip) { + if (null != ip && !ip.trim().isEmpty() && !"unknown".equalsIgnoreCase(ip) && ip.length() >= 7 && ip.length() <= 15) { + String rexp = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}"; + Pattern pat = Pattern.compile(rexp); + Matcher mat = pat.matcher(ip); + return mat.find(); + } else { + return false; + } + } + + public static String getLocalHost() { + try { + return getLocalAddress().getHostAddress(); + } catch (Exception var1) { + return "127.0.0.1"; + } + } + + public static boolean internalIp(String ip) { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + private static boolean internalIp(byte[] addr) { + byte b0 = addr[0]; + byte b1 = addr[1]; + boolean SECTION_1 = true; + boolean SECTION_2 = true; + boolean SECTION_3 = true; + boolean SECTION_4 = true; + boolean SECTION_5 = true; + boolean SECTION_6 = true; + switch (b0) { + case -84: + if (b1 >= 16 && b1 <= 31) { + return true; + } + case -64: + switch (b1) { + case -88: + return true; + } + default: + return false; + case 10: + return true; + } + } + + public static byte[] textToNumericFormatV4(String text) { + if (text.length() == 0) { + return null; + } else { + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + + try { + long l; + int i; + switch (elements.length) { + case 1: + l = Long.parseLong(elements[0]); + if (l < 0L || l > 4294967295L) { + return null; + } + + bytes[0] = (byte)((int)(l >> 24 & 255L)); + bytes[1] = (byte)((int)((l & 16777215L) >> 16 & 255L)); + bytes[2] = (byte)((int)((l & 65535L) >> 8 & 255L)); + bytes[3] = (byte)((int)(l & 255L)); + break; + case 2: + l = (long)Integer.parseInt(elements[0]); + if (l >= 0L && l <= 255L) { + bytes[0] = (byte)((int)(l & 255L)); + l = (long)Integer.parseInt(elements[1]); + if (l < 0L || l > 16777215L) { + return null; + } + + bytes[1] = (byte)((int)(l >> 16 & 255L)); + bytes[2] = (byte)((int)((l & 65535L) >> 8 & 255L)); + bytes[3] = (byte)((int)(l & 255L)); + break; + } + + return null; + case 3: + for(i = 0; i < 2; ++i) { + l = (long)Integer.parseInt(elements[i]); + if (l < 0L || l > 255L) { + return null; + } + + bytes[i] = (byte)((int)(l & 255L)); + } + + l = (long)Integer.parseInt(elements[2]); + if (l < 0L || l > 65535L) { + return null; + } + + bytes[2] = (byte)((int)(l >> 8 & 255L)); + bytes[3] = (byte)((int)(l & 255L)); + break; + case 4: + for(i = 0; i < 4; ++i) { + l = (long)Integer.parseInt(elements[i]); + if (l < 0L || l > 255L) { + return null; + } + + bytes[i] = (byte)((int)(l & 255L)); + } + + return bytes; + default: + return null; + } + + return bytes; + } catch (NumberFormatException var6) { + return null; + } + } + } + + public static String getLocalHostName() { + try { + return getLocalAddress().getHostName(); + } catch (Exception var1) { + return "未知"; + } + } + + public static InetAddress getLocalAddress() { + InetAddress candidateAddress = null; + + try { + Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); + + label60: + while(true) { + while(true) { + if (!ifaces.hasMoreElements()) { + break label60; + } + + NetworkInterface iface = (NetworkInterface)ifaces.nextElement(); + if (iface.isLoopback()) { + log.trace("NetworkInterface is Loopback:{} discard continue", JSON.toJSONString(iface)); + } else if (iface.isVirtual()) { + log.trace("NetworkInterface is Virtual:{} discard continue", JSON.toJSONString(iface)); + } else if (!iface.isUp()) { + log.trace("NetworkInterface is shutdown:{} discard continue", JSON.toJSONString(iface)); + } else { + log.trace("NetworkInterface is normal:{} continue", JSON.toJSONString(iface)); + Enumeration inetAddrs = iface.getInetAddresses(); + + while(inetAddrs.hasMoreElements()) { + InetAddress inetAddr = (InetAddress)inetAddrs.nextElement(); + if (inetAddr.isLoopbackAddress()) { + log.trace("InetAddress is Loopback:{} discard continue", JSON.toJSONString(inetAddr)); + } else { + if (inetAddr.isSiteLocalAddress()) { + candidateAddress = inetAddr; + break; + } + + log.trace("Don't want to InetAddress:{}", JSON.toJSONString(inetAddr)); + if (candidateAddress == null) { + candidateAddress = inetAddr; + } + } + } + + if (candidateAddress != null) { + break label60; + } + } + } + } + + if (candidateAddress == null) { + InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); + if (jdkSuppliedAddress != null) { + candidateAddress = jdkSuppliedAddress; + log.warn("not found non-loopback InetAddress,use default scheme(InetAddress.getLocalHost()):{}", jdkSuppliedAddress); + } else { + log.warn("use default scheme(InetAddress.getLocalHost()) is null"); + } + } + } catch (Exception var5) { + Exception e = var5; + log.error("Exception:{}", e); + } + + log.debug("IP:{},HostName:{}", candidateAddress.getHostAddress(), candidateAddress.getHostName()); + return candidateAddress; + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IdUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IdUtils.java new file mode 100644 index 0000000..a4574c1 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/IdUtils.java @@ -0,0 +1,49 @@ +package com.chushang.common.core.util; + +import cn.hutool.core.lang.id.NanoId; +import cn.hutool.crypto.digest.MD5; +import com.chushang.common.core.constant.CharConstants; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigInteger; + +/** + * by zhaowenyuan create 2021/12/29 18:00 + */ +@Slf4j +@UtilityClass +public class IdUtils { + + private final static String sign = "sanyi_cloud_sign"; + + private final static MD5 md5 = new MD5(); + + /** + * 生成 19位 数值类型 id + * 字符串转数值类型 id + * @param str 设备 id + * @param sign 签名 + * @return 根据设备码生成的 数值 + */ + public synchronized static String strConvertNum(String str,String sign) { + BigInteger bigInteger = new BigInteger(1, md5.digest(str + ":" + sign)); + return Math.abs(bigInteger.longValue()) + ""; + } + + public synchronized static String strConvertNum(String str) { + return strConvertNum(str, sign); + } + + public synchronized String getId(){ + return getId(8); + } + + public synchronized String getId(int count){ + return getId(count, CharConstants.chars); + } + + public synchronized String getId(int count, char[] chars){ + return NanoId.randomNanoId(null, chars, count); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/JwtUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/JwtUtils.java new file mode 100644 index 0000000..fbf51d9 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/JwtUtils.java @@ -0,0 +1,123 @@ +package com.chushang.common.core.util; + +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.constant.TokenConstants; +import com.chushang.common.core.text.Convert; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import java.util.Map; + +/** + * Jwt工具类 + * + * @author ruoyi + */ +public class JwtUtils +{ + public static String secret = TokenConstants.SECRET; + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + public static String createToken(Map claims) + { + return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact(); + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + public static Claims parseToken(String token) + { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); + } + + /** + * 根据令牌获取用户标识 + * + * @param token 令牌 + * @return 用户ID + */ + public static String getUserKey(String token) + { + Claims claims = parseToken(token); + return getValue(claims, SecurityConstants.USER_KEY); + } + + /** + * 根据令牌获取用户标识 + * + * @param claims 身份信息 + * @return 用户ID + */ + public static String getUserKey(Claims claims) + { + return getValue(claims, SecurityConstants.USER_KEY); + } + + /** + * 根据令牌获取用户ID + * + * @param token 令牌 + * @return 用户ID + */ + public static String getUserId(String token) + { + Claims claims = parseToken(token); + return getValue(claims, SecurityConstants.DETAILS_USER_ID); + } + + /** + * 根据身份信息获取用户ID + * + * @param claims 身份信息 + * @return 用户ID + */ + public static String getUserId(Claims claims) + { + return getValue(claims, SecurityConstants.DETAILS_USER_ID); + } + + /** + * 根据令牌获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public static String getUserName(String token) + { + Claims claims = parseToken(token); + return getValue(claims, SecurityConstants.DETAILS_USERNAME); + } + + /** + * 根据身份信息获取用户名 + * + * @param claims 身份信息 + * @return 用户名 + */ + public static String getUserName(Claims claims) + { + return getValue(claims, SecurityConstants.DETAILS_USERNAME); + } + + /** + * 根据身份信息获取键值 + * + * @param claims 身份信息 + * @param key 键 + * @return 值 + */ + public static String getValue(Claims claims, String key) + { + return Convert.toStr(claims.get(key), ""); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/MD5Util.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/MD5Util.java new file mode 100644 index 0000000..b04bb1d --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/MD5Util.java @@ -0,0 +1,127 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.core.util; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import cn.hutool.crypto.digest.MD5; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.chushang.common.core.text.ToolUtils; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MD5Util extends MD5 { + + public static String md5Sign(String jsonStr, String keyStr) throws Exception { + Map mapTemp = new HashMap<>(); + JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(JSON.parseObject(jsonStr), JSONWriter.Feature.MapSortField)); + Set> objs = jsonObject.entrySet(); + + for (Map.Entry stringObjectEntry : objs) { + if (!(stringObjectEntry.getKey().equals("sign") && !ToolUtils.isBlank(stringObjectEntry.getValue()))) { + mapTemp.put((stringObjectEntry).getKey(), stringObjectEntry.getValue()); + } + } + String strValue = genString(mapTemp, keyStr); + log.debug("signStr:" + strValue); + return calcValidateSign(strValue); + } + private static String genString(Map mapTmp, String keyStr) { + ArrayList arrayTmp = new ArrayList<>(); + + for (Map.Entry stringObjectEntry : mapTmp.entrySet()) { + String strValue = stringObjectEntry.getValue().toString(); + if (!ToolUtils.isBlank(strValue)) { + arrayTmp.add((stringObjectEntry).getKey() + "=" + stringObjectEntry.getValue() + "&"); + } + } + + String[] strArray = arrayTmp.toArray(new String[0]); + Arrays.sort(strArray); + StringBuilder strBuf = new StringBuilder(); + + for (String s : strArray) { + strBuf.append(s); + } + strBuf.append("key=").append(keyStr); + return strBuf.toString(); + } + + private static String calcValidateSign(String strValue) throws UnsupportedEncodingException { + byte[] byteString = strValue.getBytes(StandardCharsets.UTF_8); + + byte[] result; + try { + MessageDigest mdInst = MessageDigest.getInstance("MD5"); + mdInst.update(byteString); + result = mdInst.digest(); + } catch (NoSuchAlgorithmException var4) { + return null; + } + + return binaryToHexString(result).toUpperCase(); + } + private static String binaryToHexString(byte[] result) { + StringBuilder digestHexStr = new StringBuilder(); + + for(int i = 0; i < 16; ++i) { + char[] Digit = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + char[] ob = new char[]{Digit[result[i] >>> 4 & 15], Digit[result[i] & 15]}; + String s = new String(ob); + digestHexStr.append(s); + } + + return digestHexStr.toString(); + } + + public static String getMD5Encrypt(String strSrc) { + return encodeEncrypt(strSrc, "MD5"); + } + + public static String encodeEncrypt(String strSrc, String encName) { + MessageDigest md; + String strDes; + byte[] bt = strSrc.getBytes(); + + try { + if (encName == null || encName.isEmpty()) { + encName = "MD5"; + } + + md = MessageDigest.getInstance(encName); + md.update(bt); + strDes = bytes2Hex(md.digest()); + return strDes; + } catch (NoSuchAlgorithmException var6) { + log.error("Invalid algorithm."); + return null; + } + } + public static String bytes2Hex(byte[] bts) { + StringBuilder des = new StringBuilder(); + String tmp; + for (byte bt : bts) { + tmp = Integer.toHexString(bt & 255); + if (tmp.length() == 1) { + des.append("0"); + } + des.append(tmp); + } + return des.toString(); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/PackageUtil.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/PackageUtil.java new file mode 100644 index 0000000..56ecb45 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/PackageUtil.java @@ -0,0 +1,461 @@ +package com.chushang.common.core.util; + +import java.io.File; +import java.io.FileInputStream; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +/** + * 名称空间实用工具 + */ +public final class PackageUtil { + /** + * 类默认构造器 + */ + private PackageUtil() { + } + + /** + * 列出指定接口的所有实现类 + * 会将接口路径下的所有类都加载进去, 不值当 + */ + @Deprecated + public static List> getAllInterfaceAchieveClass(Class clazz) throws Throwable { + ArrayList> list = new ArrayList<>(); + // 判断是否是接口 + if (clazz.isInterface()) { + ArrayList> allClass = getAllClassByPath(clazz.getPackage().getName()); + for (Class aClass : allClass) { + // 排除抽象类 + if (Modifier.isAbstract(aClass.getModifiers())) { + continue; + } + // 判断是不是同一个接口 + if (clazz.isAssignableFrom(aClass)) { + if (!clazz.equals(aClass)) { + list.add(aClass); + } + } + } + } + return list; + } + + public static List> getAllInterfaceAchieveClassByPath(Class clazz, String packageName) throws Throwable { + if (null == clazz){ + throw new RuntimeException("clazz is null"); + } + List> list = new ArrayList<>(); + ArrayList> allClassByPath = getAllClassByPath(packageName); + if (clazz.isInterface()){ + for (Class aClass : allClassByPath) { + if (Modifier.isAbstract(aClass.getModifiers())){ + continue; + } + if (clazz.isAssignableFrom(aClass)){ + if (!clazz.equals(aClass)){ + list.add(aClass); + } + } + } + } + return list; + } + + public static ArrayList> getAllClassByPath(String packageName) throws Throwable { + ArrayList> list = new ArrayList<>(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + String path = packageName.replace('.', '/'); + ArrayList fileList = new ArrayList<>(); + Enumeration enumeration = classLoader.getResources(path); + while (enumeration.hasMoreElements()) { + URL url = enumeration.nextElement(); + fileList.add(new File(url.getFile())); + } + for (File file : fileList) { + list.addAll(findClass(file, packageName)); + } + return list; + } + + private static ArrayList> findClass(File file, String packageName) throws ClassNotFoundException { + ArrayList> list = new ArrayList<>(); + if (!file.exists()) { + return list; + } + File[] files = file.listFiles(); + assert files != null; + for (File file2 : files) { + if (file2.isDirectory()) { + assert !file2.getName().contains("."); + ArrayList> arrayList = findClass(file2, packageName + "." + file2.getName()); + list.addAll(arrayList); + } else if (file2.getName().endsWith(".class")) { + // 保存的类文件不需要后缀.class + list.add(Class.forName(packageName + '.' + file2.getName().substring(0, file2.getName().length() - 6))); + } + } + return list; + } + + /** + * 列表指定包中的所有子类 + * + * @param packageName 包名称 + * @param recursive 是否递归查找 + * @param superClazz 父类的类型 + * @return 子类集合 + */ + public static Set> listSubClazz( + String packageName, + boolean recursive, + Class superClazz) { + if (superClazz == null) { + return Collections.emptySet(); + } else { + return listClazz(packageName, recursive, superClazz::isAssignableFrom); + } + } + + /** + * 列表指定包中的所有类 + * + * @param packageName 包名称 + * @param recursive 是否递归查找? + * @param filter 过滤器 + * @return 符合条件的类集合 + */ + public static Set> listClazz( + String packageName, boolean recursive, IClazzFilter filter) { + + if (packageName == null || + packageName.isEmpty()) { + return null; + } + + // 将点转换成斜杠 + final String packagePath = packageName.replace('.', '/'); + // 获取类加载器 + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + // 结果集合 + Set> resultSet = new HashSet<>(); + + try { + // 获取 URL 枚举 + Enumeration urlEnum = cl.getResources(packagePath); + + while (urlEnum.hasMoreElements()) { + // 获取当前 URL + URL currUrl = urlEnum.nextElement(); + // 获取协议文本 + final String protocol = currUrl.getProtocol(); + // 定义临时集合 + Set> tmpSet = null; + + if ("FILE".equalsIgnoreCase(protocol)) { + // 从文件系统中加载类 + tmpSet = listClazzFromDir( + new File(currUrl.getFile()), packageName, recursive, filter + ); + } else if ("JAR".equalsIgnoreCase(protocol)) { + // 获取文件字符串 + String fileStr = currUrl.getFile(); + + if (fileStr.startsWith("file:")) { + // 如果是以 "file:" 开头的, + // 则去除这个开头 + fileStr = fileStr.substring(5); + } + + if (fileStr.lastIndexOf('!') > 0) { + // 如果有 '!' 字符, + // 则截断 '!' 字符之后的所有字符 + fileStr = fileStr.substring(0, fileStr.lastIndexOf('!')); + } + + // 从 JAR 文件中加载类 + tmpSet = listClazzFromJar( + new File(fileStr), packageName, recursive, filter + ); + } + + if (tmpSet != null) { + // 如果类集合不为空, + // 则添加到结果中 + resultSet.addAll(tmpSet); + } + } + } catch (Exception ex) { + // 抛出异常! + throw new RuntimeException(ex); + } + + return resultSet; + } + + /** + * 从目录中获取类列表 + * + * @param dirFile 目录 + * @param packageName 包名称 + * @param recursive 是否递归查询子包 + * @param filter 类过滤器 + * @return 符合条件的类集合 + */ + private static Set> listClazzFromDir( + final File dirFile, final String packageName, final boolean recursive, IClazzFilter filter) { + + if (!dirFile.exists() || + !dirFile.isDirectory()) { + // 如果参数对象为空, + // 则直接退出! + return null; + } + + // 获取子文件列表 + File[] subFileArr = dirFile.listFiles(); + + if (subFileArr == null || + subFileArr.length <= 0) { + return null; + } + + // 文件队列, 将子文件列表添加到队列 + Queue fileQ = new LinkedList<>(Arrays.asList(subFileArr)); + + // 结果对象 + Set> resultSet = new HashSet<>(); + + while (!fileQ.isEmpty()) { + // 从队列中获取文件 + File currFile = fileQ.poll(); + + if (currFile.isDirectory() && + recursive) { + // 如果当前文件是目录, + // 并且是执行递归操作时, + // 获取子文件列表 + subFileArr = currFile.listFiles(); + + if (subFileArr != null && + subFileArr.length > 0) { + // 添加文件到队列 + fileQ.addAll(Arrays.asList(subFileArr)); + } + continue; + } + + if (!currFile.isFile() || + !currFile.getName().endsWith(".class")) { + // 如果当前文件不是文件, + // 或者文件名不是以 .class 结尾, + // 则直接跳过 + continue; + } + + // 类名称 + String clazzName; + + // 设置类名称 + clazzName = currFile.getAbsolutePath(); + // 清除最后的 .class 结尾 + clazzName = clazzName.substring(dirFile.getAbsolutePath().length(), clazzName.lastIndexOf('.')); + // 转换目录斜杠 + clazzName = clazzName.replace('\\', '/'); + // 清除开头的 / + clazzName = trimLeft(clazzName, "/"); + // 将所有的 / 修改为 . + clazzName = join(clazzName.split("/"), "."); + // 包名 + 类名 + clazzName = packageName + "." + clazzName; + + try { + // 加载类定义 + Class clazzObj = Class.forName(clazzName); + + if (null != filter && + !filter.accept(clazzObj)) { + // 如果过滤器不为空, + // 且过滤器不接受当前类, + // 则直接跳过! + continue; + } + + // 添加类定义到集合 + resultSet.add(clazzObj); + } catch (Exception ex) { + // 抛出异常 + throw new RuntimeException(ex); + } + } + + return resultSet; + } + + /** + * 从 .jar 文件中获取类列表 + * + * @param jarFilePath .jar 文件路径 + * @param recursive 是否递归查询子包 + * @param filter 类过滤器 + * @return 符合条件的类集合 + */ + private static Set> listClazzFromJar( + final File jarFilePath, final String packageName, final boolean recursive, IClazzFilter filter) { + + if (jarFilePath == null || + jarFilePath.isDirectory()) { + // 如果参数对象为空, + // 则直接退出! + return null; + } + + // 结果对象 + Set> resultSet = new HashSet<>(); + + try { + // 创建 .jar 文件读入流 + JarInputStream jarIn = new JarInputStream(new FileInputStream(jarFilePath)); + // 进入点 + JarEntry entry; + + while ((entry = jarIn.getNextJarEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + + // 获取进入点名称 + String entryName = entry.getName(); + + if (!entryName.endsWith(".class")) { + // 如果不是以 .class 结尾, + // 则说明不是 JAVA 类文件, 直接跳过! + continue; + } + + if (!recursive) { + // + // 如果没有开启递归模式, + // 那么就需要判断当前 .class 文件是否在指定目录下? + // + // 获取目录名称 + String tmpStr = entryName.substring(0, entryName.lastIndexOf('/')); + // 将目录中的 "/" 全部替换成 "." + tmpStr = join(tmpStr.split("/"), "."); + + if (!packageName.equals(tmpStr)) { + // 如果包名和目录名不相等, + // 则直接跳过! + continue; + } + } + + String clazzName; + + // 清除最后的 .class 结尾 + clazzName = entryName.substring(0, entryName.lastIndexOf('.')); + // 将所有的 / 修改为 . + clazzName = join(clazzName.split("/"), "."); + + // 加载类定义 + Class clazzObj = Class.forName(clazzName); + + if (null != filter && + !filter.accept(clazzObj)) { + // 如果过滤器不为空, + // 且过滤器不接受当前类, + // 则直接跳过! + continue; + } + + // 添加类定义到集合 + resultSet.add(clazzObj); + } + + // 关闭 jar 输入流 + jarIn.close(); + } catch (Exception ex) { + // 抛出异常 + throw new RuntimeException(ex); + } + + return resultSet; + } + + /** + * 类名称过滤器 + * + * @author hjj2019 + */ + @FunctionalInterface + private interface IClazzFilter { + /** + * 是否接受当前类? + * + * @param clazz 被筛选的类 + * @return 是否符合条件 + */ + boolean accept(Class clazz); + } + + /** + * 使用连接符连接字符串数组 + * + * @param strArr 字符串数组 + * @param conn 连接符 + * @return 连接后的字符串 + */ + private static String join(String[] strArr, String conn) { + if (null == strArr || + strArr.length <= 0) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < strArr.length; i++) { + if (i > 0) { + // 添加连接符 + sb.append(conn); + } + + // 添加字符串 + sb.append(strArr[i]); + } + + return sb.toString(); + } + + /** + * 清除源字符串左边的字符串 + * + * @param src 原字符串 + * @param trimStr 需要被清除的字符串 + * @return 清除后的字符串 + */ + private static String trimLeft(String src, String trimStr) { + if (null == src || + src.isEmpty()) { + return ""; + } + + if (null == trimStr || + trimStr.isEmpty()) { + return src; + } + + if (src.equals(trimStr)) { + return ""; + } + + while (src.startsWith(trimStr)) { + src = src.substring(trimStr.length()); + } + + return src; + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/RequestUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/RequestUtils.java new file mode 100644 index 0000000..6a2b348 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/RequestUtils.java @@ -0,0 +1,50 @@ +package com.chushang.common.core.util; + +import lombok.experimental.UtilityClass; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; + +/** + * by zhaowenyuan create 2021/12/30 18:26 + * HttpServletRequest 获取工具类 + */ +@UtilityClass +public class RequestUtils { + public String ReadAsChars(HttpServletRequest request) + { + BufferedReader br = null; + StringBuilder sb = new StringBuilder(); + try + { + br = request.getReader(); + String str; + while ((str = br.readLine()) != null) + { + sb.append(str); + } + br.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + finally + { + if (null != br) + { + try + { + br.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + } + return sb.toString(); + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ServletUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ServletUtils.java new file mode 100644 index 0000000..61cfd99 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/ServletUtils.java @@ -0,0 +1,264 @@ +package com.chushang.common.core.util; + +import cn.hutool.json.JSONUtil; +import com.chushang.common.core.text.Convert; +import com.chushang.common.core.web.Result; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import reactor.core.publisher.Mono; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 客户端工具类 + * + * @author ruoyi + */ +public class ServletUtils +{ + /** + * 获取String参数 + */ + public static String getParameter(String name) + { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) + { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) + { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) + { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) + { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) + { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + + /** + * https://blog.csdn.net/nlx1450161741/article/details/108266393 异步操作导致异步线程获取不到主线程的request信息 + */ + public static HttpServletRequest getRequest(){ + return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + } + + public static RequestAttributes getRequestAttributes(){ + return RequestContextHolder.getRequestAttributes(); + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + + public static String getHeader(HttpServletRequest request, String name) + { + String value = request.getHeader(name); + if (StringUtils.isEmpty(value)) + { + return StringUtils.EMPTY; + } + return urlDecode(value); + } + + public static Map getHeaders(HttpServletRequest request) + { + Map map = new LinkedHashMap<>(); + Enumeration enumeration = request.getHeaderNames(); + if (enumeration != null) + { + while (enumeration.hasMoreElements()) + { + String key = enumeration.nextElement(); + String value = request.getHeader(key); + map.put(key, value); + } + } + return map; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + return URLEncoder.encode(str, StandardCharsets.UTF_8); + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + return URLDecoder.decode(str, StandardCharsets.UTF_8); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value) + { + return webFluxResponseWriter(response, HttpStatus.OK, value, Result.FAIL_CODE); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value, int code) + { + return webFluxResponseWriter(response, HttpStatus.OK, value, code); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param status http状态码 + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code) + { + return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code); + } + + /** + * 根据状态码 进行设置 + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, HttpStatus status) + { + return webFluxResponseWriter(response, HttpStatus.OK, status.getReasonPhrase(), status.value()); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param contentType content-type + * @param status http状态码 + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code) + { + response.setStatusCode(status); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType); + Result result = Result.failed(code, value + ""); + DataBuffer dataBuffer = response.bufferFactory().wrap(JSONUtil.toJsonStr(result).getBytes()); + return response.writeWith(Mono.just(dataBuffer)); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/SpringUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/SpringUtils.java new file mode 100644 index 0000000..ebdde37 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/SpringUtils.java @@ -0,0 +1,88 @@ +package com.chushang.common.core.util; + +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.stereotype.Component; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author ruoyi + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + /** + * 获取对象 + * @return Object 一个以所给名字注册的bean的实例 + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + */ + public static T getBean(Class clz) throws BeansException + { + return (T) beanFactory.getBean(clz); + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + return (T) AopContext.currentProxy(); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/StringUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/StringUtils.java new file mode 100644 index 0000000..6c6356d --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/util/StringUtils.java @@ -0,0 +1,537 @@ +package com.chushang.common.core.util; + +import com.chushang.common.core.text.StrFormatter; +import com.chushang.common.core.constant.Constants; +import org.springframework.util.AntPathMatcher; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * 字符串工具类 + * + * @author ruoyi + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils +{ + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + private static final Pattern NUMBER_PATTERN = Pattern.compile("-?[0-9]+(\\\\.[0-9]+)?"); + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) + { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) + { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) + { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + ** @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) + { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) + { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) + { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) + { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) + { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) + { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) + { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) + { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) + { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) + { + if (str == null) + { + return NULLSTR; + } + + if (start < 0) + { + start = str.length() + start; + } + + if (start < 0) + { + start = 0; + } + if (start > str.length()) + { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) + { + if (str == null) + { + return NULLSTR; + } + + if (end < 0) + { + end = str.length() + end; + } + if (start < 0) + { + start = str.length() + start; + } + + if (end > str.length()) + { + end = str.length(); + } + + if (start > end) + { + return NULLSTR; + } + + if (start < 0) + { + start = 0; + } + if (end < 0) + { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) + { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) + { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) + { + if (!Character.isWhitespace(str.charAt(i))) + { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) + { + if (isEmpty(params) || isEmpty(template)) + { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean isHttp(String link) + { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) + { + if (str == null) + { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) + { + char c = str.charAt(i); + if (i > 0) + { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } + else + { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) + { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) + { + sb.append(SEPARATOR); + } + else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) + { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) + { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) + { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) + { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) + { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 例如:user_name->userName + */ + public static String toCamelCase(String s) + { + if (s == null) + { + return null; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + + if (c == SEPARATOR) + { + upperCase = true; + } + else if (upperCase) + { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } + else + { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) + { + if (isEmpty(str) || isEmpty(strs)) + { + return false; + } + for (String pattern : strs) + { + if (isMatch(pattern, str)) + { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + */ + public static boolean isMatch(String pattern, String url) + { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) + { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static String padl(final Number num, final int size) + { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static String padl(final String s, final int size, final char c) + { + final StringBuilder sb = new StringBuilder(size); + if (s != null) + { + final int len = s.length(); + if (s.length() <= size) + { + sb.append(String.valueOf(c).repeat(size - len)); + sb.append(s); + } + else + { + return s.substring(len - size, len); + } + } + else + { + sb.append(String.valueOf(c).repeat(Math.max(0, size))); + } + return sb.toString(); + } + + /** + * 判断是否为数值类型的字符串 + */ + public static boolean isInteger(String str) { + if (isEmpty(str)){ + return false; + } + return NUMBER_PATTERN.matcher(str).matches(); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/AjaxResult.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/AjaxResult.java new file mode 100644 index 0000000..c9f03c2 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/AjaxResult.java @@ -0,0 +1,181 @@ +package com.chushang.common.core.web; + +import cn.hutool.http.HttpStatus; +import com.chushang.common.core.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 操作消息提醒 + * + * @author ruoyi + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + public static final String SUCCESS = "success"; + + public static final String FAIL = "Server internal error"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + } + + /** + * 方便链式调用 + */ +// @Override +// public AjaxResult put(String key, Object value) +// { +// super.put(key, value); +// return this; +// } + + public AjaxResult add(String key, Object value){ + super.put(key, value); + return this; + } + + public AjaxResult addAll(Map map){ + super.putAll(map); + return this; + } + + + public static AjaxResult Builder(){ + return new AjaxResult(); + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success(SUCCESS); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success(SUCCESS, data); + } + + /** + * 返回成功消息 + * @param data 返回内容 + */ + public static AjaxResult success(String data) + { + return AjaxResult.success(SUCCESS, data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.HTTP_OK, msg, data); + } + + /** + * 返回错误消息 + * + * @return + */ + public static AjaxResult error() + { + return AjaxResult.error(FAIL); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.HTTP_INTERNAL_ERROR, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + public boolean isSuccess(){ + return HttpStatus.HTTP_OK == (int)this.get(CODE_TAG); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/EnumUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/EnumUtils.java new file mode 100644 index 0000000..370d77e --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/EnumUtils.java @@ -0,0 +1,31 @@ +package com.chushang.common.core.web; + +import lombok.experimental.UtilityClass; + +import java.io.Serializable; + +/** + * by zhaowenyuan create 2022/1/10 14:28 + * 枚举获取帮助类 , 要求 enum 类必须 实现 CodeEnum, 包含有 Code , 并且 Code 唯一 + */ +@UtilityClass +public class EnumUtils { + public static > T getByCode(Serializable code, Class tClass) { + if (null == code){ + return null; + } + for (T each : tClass.getEnumConstants()) { + if (code.equals(each.getCode())) { + return each; + } + } + return null; + } + + public interface CodeEnum { + C getCode(); + S getMsg(); + } +} + + diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/Result.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/Result.java new file mode 100644 index 0000000..77dc270 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/Result.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.web; + +import lombok.*; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 响应信息主体 + * + * @author lengleng + */ +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class Result implements Serializable { + public static final int SUCCESS_CODE = 200; + public static final String SUCCESS_MSG = "success"; + public static final String FAIL_MSG = "fail"; + public static final int FAIL_CODE = 500; + + + private static final long serialVersionUID = 1L; + + @Getter + @Setter + private int code; + + @Getter + @Setter + private String msg; + + @Getter + @Setter + private T data; + + public static Result> putAll(Map params) { + return Result.responseData() + .addParams(params) + .build(); + } + + public static Result> put(String key, Object data) { + return Result.responseData() + .addParam(key, data) + .build(); + } + + public static Result ok() { + return restResult(null, SUCCESS_CODE, SUCCESS_MSG); + } + + public static Result ok(T data) { + return restResult(data, SUCCESS_CODE, SUCCESS_MSG); + } + + public static Result ok(T data, String msg) { + return restResult(data, SUCCESS_CODE, SUCCESS_MSG); + } + + public static Result failed() { + return restResult(null, FAIL_CODE, FAIL_MSG); + } + + public static Result failed(String msg) { + return restResult(null, FAIL_CODE, msg); + } + + public static Result failedFeign(){ + return restResult(null, FAIL_CODE, "请稍后再试"); + } + + public static Result failed(T data) { + return restResult(data, FAIL_CODE, FAIL_MSG); + } + + public static Result failed(T data, String msg) { + return restResult(data, FAIL_CODE, msg); + } + + public static Result failed(int code, String msg) { + return restResult(null, code, msg); + } + + public static Result restResult(T data, int code, String msg) { + Result apiResult = new Result<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + public static ResultMap responseData(){ + return new ResultMap(); + } + + public static class ResultMap { + private final Map resultMap = new HashMap<>(); + + public ResultMap addParam(String key, Object value){ + this.resultMap.put(key, value); + return this; + } + + public ResultMap addParams(Map params) { + this.resultMap.putAll(params); + return this; + } + + public Result> build(){ + return Result.ok(this.resultMap); + } + + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/TokenUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/TokenUtils.java new file mode 100644 index 0000000..ac847dc --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/TokenUtils.java @@ -0,0 +1,36 @@ +package com.chushang.common.core.web; + +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; +import cn.hutool.crypto.symmetric.SymmetricCrypto; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * by zhaowenyuan create 2021/12/30 11:42 + */ +@Slf4j +@UtilityClass +public class TokenUtils { + private final String TOKEN_SIGN = "sane_cloud_token"; + + public String decryptStr(String sanyiToken){ + SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, TOKEN_SIGN.getBytes(StandardCharsets.UTF_8)); + try { + return aes.decryptStr(sanyiToken); + }catch (Exception ignored){ + } + return null; + } + + public static String decryptStr(String sanyiToken, String signType) { + // SymmetricAlgorithm.valueof() 必定不为空, 并且不会异常 + try { + SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.valueOf(signType), TOKEN_SIGN.getBytes(StandardCharsets.UTF_8)); + return aes.decryptStr(sanyiToken); + }catch (Exception ignored){ + } + return null; + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/ValidList.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/ValidList.java new file mode 100644 index 0000000..904bd31 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/ValidList.java @@ -0,0 +1,134 @@ +package com.chushang.common.core.web; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.*; + +/** + * by zhaowenyuan create 2022/2/22 09:38 + */ +public class ValidList implements List, Serializable { + private static final long serialVersionUID = 1L; + + @Valid + @Size(min = 1, message = "集合不能为空") + @NotNull(message = "集合不能为null") + private final List list = new ArrayList<>(); + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator iterator() { + return list.iterator(); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(E e) { + return list.add(e); + } + + @Override + public boolean remove(Object o) { + return list.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + return list.addAll(index,c); + } + + @Override + public boolean removeAll(Collection c) { + return list.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return list.retainAll(c); + } + + @Override + public void clear() { + list.clear(); + } + + @Override + public E get(int index) { + return list.get(index); + } + + @Override + public E set(int index, E element) { + return list.set(index,element); + } + + @Override + public void add(int index, E element) { + list.add(index, element); + } + + @Override + public E remove(int index) { + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return list.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return list.listIterator(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return list.subList(fromIndex,toIndex); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/WebUtils.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/WebUtils.java new file mode 100644 index 0000000..d330cb9 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/web/WebUtils.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.core.web; + +import cn.hutool.core.codec.Base64; +import cn.hutool.json.JSONUtil; +import com.chushang.common.core.util.ClassUtils; +import com.chushang.common.core.exception.CheckedException; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +/** + * Miscellaneous utilities for web applications. + * + * @author L.cm + */ +@Slf4j +@UtilityClass +public class WebUtils extends org.springframework.web.util.WebUtils { + + private final String BASIC_ = "Basic "; + + private final String UNKNOWN = "unknown"; + + /** + * 判断是否ajax请求 spring ajax 返回含有 ResponseBody 或者 RestController注解 + * @param handlerMethod HandlerMethod + * @return 是否ajax请求 + */ + public boolean isBody(HandlerMethod handlerMethod) { + ResponseBody responseBody = ClassUtils.getAnnotation(handlerMethod, ResponseBody.class); + return responseBody != null; + } + + /** + * 读取cookie + * @param name cookie name + * @return cookie value + */ + public String getCookieVal(String name) { + HttpServletRequest request = WebUtils.getRequest(); + Assert.notNull(request, "request from RequestContextHolder is null"); + return getCookieVal(request, name); + } + + /** + * 读取cookie + * @param request HttpServletRequest + * @param name cookie name + * @return cookie value + */ + public String getCookieVal(HttpServletRequest request, String name) { + Cookie cookie = getCookie(request, name); + return cookie != null ? cookie.getValue() : null; + } + + /** + * 清除 某个指定的cookie + * @param response HttpServletResponse + * @param key cookie key + */ + public void removeCookie(HttpServletResponse response, String key) { + setCookie(response, key, null, 0); + } + + /** + * 设置cookie + * @param response HttpServletResponse + * @param name cookie name + * @param value cookie value + * @param maxAgeInSeconds maxage + */ + public void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setMaxAge(maxAgeInSeconds); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + +// /** +// * 获取 HttpServletRequest +// * @return {HttpServletRequest} +// */ +// public Optional getRequest() { +// return Optional +// .ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()); +// } + + /** + * 获取 HttpServletRequest + * @return {HttpServletRequest} + */ + public HttpServletRequest getRequest() { + return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + } + + /** + * 获取 HttpServletResponse + * @return {HttpServletResponse} + */ + public HttpServletResponse getResponse() { + return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); + } + + /** + * 返回json + * @param response HttpServletResponse + * @param result 结果对象 + */ + public void renderJson(HttpServletResponse response, Object result) { + renderJson(response, result, MediaType.APPLICATION_JSON_VALUE); + } + + /** + * 返回json + * @param response HttpServletResponse + * @param result 结果对象 + * @param contentType contentType + */ + public void renderJson(HttpServletResponse response, Object result, String contentType) { + response.setCharacterEncoding("UTF-8"); + response.setContentType(contentType); + try (PrintWriter out = response.getWriter()) { + out.append(JSONUtil.toJsonStr(result)); + } + catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + /** + * 从request 获取CLIENT_ID + * @return + */ + @SneakyThrows + public String getClientId(ServerHttpRequest request) { + String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + + return splitClient(header)[0]; + } + + @SneakyThrows + public String getClientId(HttpServletRequest request) { + String header = WebUtils.getRequest().getHeader("Authorization"); + + return splitClient(header)[0]; + } + + @NotNull + private static String[] splitClient(String header) throws UnsupportedEncodingException { + if (header == null || !header.startsWith(BASIC_)) { + throw new CheckedException("请求头中client信息为空"); + } + byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); + byte[] decoded; + try { + decoded = Base64.decode(base64Token); + } + catch (IllegalArgumentException e) { + throw new CheckedException("Failed to decode basic authentication token"); + } + + String token = new String(decoded, StandardCharsets.UTF_8); + + int delim = token.indexOf(":"); + + if (delim == -1) { + throw new CheckedException("Invalid basic authentication token"); + } + return new String[] { token.substring(0, delim), token.substring(delim + 1) }; + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/EscapeUtil.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/EscapeUtil.java new file mode 100644 index 0000000..a418c2d --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/EscapeUtil.java @@ -0,0 +1,155 @@ +package com.chushang.common.core.xss; + +import com.chushang.common.core.util.StringUtils; + +/** + * 转义和反转义工具类 + * + * @author ruoyi + */ +public class EscapeUtil +{ + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static + { + for (int i = 0; i < 64; i++) + { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) + { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) + { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) + { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) + { + if (StringUtils.isEmpty(text)) + { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) + { + c = text.charAt(i); + if (c < 256) + { + tmp.append("%"); + if (c < 16) + { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + else + { + tmp.append("%u"); + if (c <= 0xfff) + { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) + { + if (StringUtils.isEmpty(content)) + { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) + { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) + { + if (content.charAt(pos + 1) == 'u') + { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } + else + { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } + else + { + if (pos == -1) + { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } + else + { + tmp.append(content, lastPos, pos); + lastPos = pos; + } + } + } + return tmp.toString(); + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/HTMLFilter.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/HTMLFilter.java new file mode 100644 index 0000000..d7dc75e --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/HTMLFilter.java @@ -0,0 +1,531 @@ +package com.chushang.common.core.xss; + +import com.chushang.common.core.constant.CommonConstants; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * HTML filtering utility for protecting against XSS (Cross Site Scripting). + * + * This code is licensed LGPLv3 + * + * This code is a Java port of the original work in PHP by Cal Hendersen. + * http://code.iamcal.com/php/lib_filter/ + * + * The trickiest part of the translation was handling the differences in regex handling + * between PHP and Java. These resources were helpful in the process: + * + * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html + * http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php + * http://www.regular-expressions.info/modifiers.html + * + * A note on naming conventions: instance variables are prefixed with a "v"; global + * constants are in all caps. + * + * Sample use: + * String input = ... + * String clean = new HTMLFilter().filter( input ); + * + * The class is not thread safe. Create a new instance if in doubt. + * + * If you find bugs or have suggestions on improvement (especially regarding + * performance), please contact us. The latest version of this + * source, and our contact details, can be found at http://xss-html-filter.sf.net + * + * @author Joseph O'Connell + * @author Cal Hendersen + * @author Michael Semb Wever + */ +public final class HTMLFilter { + + /** regex flag union representing /si modifiers in php **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("<"); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** set of allowed html elements, along with allowed attributes for each element **/ + private final Map> vAllowed; + /** counts of open tags for each (allowable) html element **/ + private final Map vTagCounts = new HashMap<>(); + + /** html elements which must always be self-closing (e.g. "") **/ + private final String[] vSelfClosingTags; + /** html elements which must always have separate opening and closing tags (e.g. "") **/ + private final String[] vNeedClosingTags; + /** set of disallowed html elements **/ + private final String[] vDisallowed; + /** attributes which should be checked for valid protocols **/ + private final String[] vProtocolAtts; + /** allowed protocols **/ + private final String[] vAllowedProtocols; + /** tags which should be removed if they contain no content (e.g. "" or "") **/ + private final String[] vRemoveBlanks; + /** entities allowed within html markup **/ + private final String[] vAllowedEntities; + /** flag determining whether comments are allowed in input String. */ + private final boolean stripComment; + private final boolean encodeQuotes; + private boolean vDebug = false; + /** + * flag determining whether to try to make tags when presented with "unbalanced" + * angle brackets (e.g. "" becomes " text "). If set to false, + * unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** Default constructor. + * + */ + public HTMLFilter() { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[]{"img"}; + vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; + vDisallowed = new String[]{}; + vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp. + vProtocolAtts = new String[]{"src", "href"}; + vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; + vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** Set debug flag to true. Otherwise use default settings. See the default constructor. + * + * @param debug turn debug on with a true argument + */ + public HTMLFilter(final boolean debug) { + this(); + vDebug = debug; + + } + + /** Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + public HTMLFilter(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + private void debug(final String msg) { + if (vDebug) { + Logger.getAnonymousLogger().info(msg); + } + } + + //--------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + //--------------------------------------------------------------- + /** + * given a user submitted input String, filter out any invalid or restricted + * html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + debug("************************************************"); + debug(" INPUT: " + input); + + s = escapeComments(s); + debug(" escapeComments: " + s); + + s = balanceHTML(s); + debug(" balanceHTML: " + s); + + s = checkTags(s); + debug(" checkTags: " + s); + + s = processRemoveBlanks(s); + debug("processRemoveBlanks: " + s); + + s = validateEntities(s); + debug(" validateEntites: " + s); + + debug("************************************************\n\n"); + return s; + } + + public boolean isAlwaysMakeTags(){ + return alwaysMakeTags; + } + + public boolean isStripComments(){ + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); //(.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if(!P_REMOVE_PAIR_BLANKS.containsKey(tag)){ + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if(!P_REMOVE_SELF_BLANKS.containsKey(tag)){ + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (!inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + //debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) { + StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) { + paramNames.add(m2.group(1)); //([a-z0-9]+) + paramValues.add(m2.group(3)); //(.*?) + } + while (m3.find()) { + paramNames.add(m3.group(1)); //([a-z0-9]+) + paramValues.add(m3.group(3)); //([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + +// debug( "paramName='" + paramName + "'" ); +// debug( "paramValue='" + paramValue + "'" ); +// debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params.append(" ").append(paramName).append("=\"").append(paramValue).append("\""); + } + } + + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); //([^&;]*) + final String two = m.group(2); //(?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s){ + if(encodeQuotes){ + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); //(>|^) + final String two = m.group(2); //([^<]+?) + final String three = m.group(3); //(<|$) + m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three)); + } + m.appendTail(buf); + return buf.toString(); + }else{ + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return CommonConstants.SEMICOLON.equals(term) && isValidEntity(preamble) + ? '&' + preamble + : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/SQLFilter.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/SQLFilter.java new file mode 100644 index 0000000..18d1c00 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/SQLFilter.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2016-2019 人人开源 All rights reserved. + * + * https://www.renren.io + * + * 版权所有,侵权必究! + */ + +package com.chushang.common.core.xss; + +import org.apache.commons.lang3.StringUtils; + +import com.chushang.common.core.constant.CommonConstants; +import com.chushang.common.core.exception.ResultException; + +/** + * SQL过滤 + * + * @author Mark sunlightcs@gmail.com + */ +public class SQLFilter { + + /** + * SQL注入过滤 + * @param str 待验证的字符串 + */ + public static String sqlInject(String str){ + if(StringUtils.isBlank(str)){ + return null; + } + //去掉'|"|;|\字符 + str = StringUtils.replace(str, CommonConstants.SINGLE_QUOTATION_MARKS, ""); + str = StringUtils.replace(str, CommonConstants.DOUBLE_QUOTATION_MARKS, ""); + str = StringUtils.replace(str, CommonConstants.SEMICOLON, ""); + str = StringUtils.replace(str, CommonConstants.DOUBLE_SLASH, ""); + + //转换成小写 + str = str.toLowerCase(); + + //非法字符 + String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"}; + + //判断是否包含非法字符 + for(String keyword : keywords){ + if(str.contains(keyword)){ + throw new ResultException("包含非法字符"); + } + } + + return str; + } +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/Xss.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/Xss.java new file mode 100644 index 0000000..2e8cd93 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/Xss.java @@ -0,0 +1,27 @@ +package com.chushang.common.core.xss; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssFilter.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssFilter.java new file mode 100644 index 0000000..a47c798 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssFilter.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2016-2019 人人开源 All rights reserved. + * + * https://www.renren.io + * + * 版权所有,侵权必究! + */ + +package com.chushang.common.core.xss; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * XSS过滤 + * + * @author Mark sunlightcs@gmail.com + */ +public class XssFilter implements Filter { + + @Override + public void init(FilterConfig config) throws ServletException { + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper( + (HttpServletRequest) request); + + chain.doFilter(xssRequest, response); + } + + @Override + public void destroy() { + } + +} \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssHttpServletRequestWrapper.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..3e8c0a8 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssHttpServletRequestWrapper.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2016-2019 人人开源 All rights reserved. + * + * https://www.renren.io + * + * 版权所有,侵权必究! + */ + +package com.chushang.common.core.xss; + +import io.micrometer.core.instrument.util.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * XSS过滤处理 + * + * @author Mark sunlightcs@gmail.com + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { + //没被包装过的HttpServletRequest(特殊场景,需要自己过滤) + HttpServletRequest orgRequest; + //html过滤 + private final static HTMLFilter htmlFilter = new HTMLFilter(); + + public XssHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + orgRequest = request; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + //非json类型,直接返回 + if(!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))){ + return super.getInputStream(); + } + + //为空,直接返回 + String json = IOUtils.toString(super.getInputStream(),Charset.defaultCharset()); + if (StringUtils.isBlank(json)) { + return super.getInputStream(); + } + + //xss过滤 + json = xssEncode(json); + final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + + @Override + public int read() throws IOException { + return bis.read(); + } + }; + } + + @Override + public String getParameter(String name) { + String value = super.getParameter(xssEncode(name)); + if (StringUtils.isNotBlank(value)) { + value = xssEncode(value); + } + return value; + } + + @Override + public String[] getParameterValues(String name) { + String[] parameters = super.getParameterValues(name); + if (parameters == null || parameters.length == 0) { + return null; + } + + for (int i = 0; i < parameters.length; i++) { + parameters[i] = xssEncode(parameters[i]); + } + return parameters; + } + + @Override + public Map getParameterMap() { + Map map = new LinkedHashMap<>(); + Map parameters = super.getParameterMap(); + for (String key : parameters.keySet()) { + String[] values = parameters.get(key); + for (int i = 0; i < values.length; i++) { + values[i] = xssEncode(values[i]); + } + map.put(key, values); + } + return map; + } + + @Override + public String getHeader(String name) { + String value = super.getHeader(xssEncode(name)); + if (StringUtils.isNotBlank(value)) { + value = xssEncode(value); + } + return value; + } + + private String xssEncode(String input) { + return htmlFilter.filter(input); + } + + /** + * 获取最原始的request + */ + public HttpServletRequest getOrgRequest() { + return orgRequest; + } + + /** + * 获取最原始的request + */ + public static HttpServletRequest getOrgRequest(HttpServletRequest request) { + if (request instanceof XssHttpServletRequestWrapper) { + return ((XssHttpServletRequestWrapper) request).getOrgRequest(); + } + + return request; + } + +} diff --git a/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssValidator.java b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssValidator.java new file mode 100644 index 0000000..6ca08a9 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/java/com/chushang/common/core/xss/XssValidator.java @@ -0,0 +1,35 @@ +package com.chushang.common.core.xss; + +import org.apache.commons.lang3.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author ruoyi + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + return matcher.matches(); + } +} \ No newline at end of file diff --git a/chushang-common/chushang-common-core/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a8db6f4 --- /dev/null +++ b/chushang-common/chushang-common-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.common.core.util.SpringUtils, \ + com.chushang.common.core.handler.GlobalExceptionHandler diff --git a/chushang-common/chushang-common-easy-es/pom.xml b/chushang-common/chushang-common-easy-es/pom.xml new file mode 100644 index 0000000..35eace9 --- /dev/null +++ b/chushang-common/chushang-common-easy-es/pom.xml @@ -0,0 +1,30 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common-easy-es + + + + + org.dromara.easy-es + easy-es-boot-starter + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + + + org.elasticsearch + elasticsearch + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-easy-es/src/main/java/com/chushang/common/ee/EasyEsConfig.java b/chushang-common/chushang-common-easy-es/src/main/java/com/chushang/common/ee/EasyEsConfig.java new file mode 100644 index 0000000..33bd0a8 --- /dev/null +++ b/chushang-common/chushang-common-easy-es/src/main/java/com/chushang/common/ee/EasyEsConfig.java @@ -0,0 +1,8 @@ +package com.chushang.common.ee; + + +import org.dromara.easyes.starter.register.EsMapperScan; + +@EsMapperScan("com.chushang.**.mapper") +public class EasyEsConfig { +} diff --git a/chushang-common/chushang-common-easy-es/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-easy-es/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..dd6be35 --- /dev/null +++ b/chushang-common/chushang-common-easy-es/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.common.ee.EasyEsConfig diff --git a/chushang-common/chushang-common-excel/pom.xml b/chushang-common/chushang-common-excel/pom.xml new file mode 100644 index 0000000..34a3e20 --- /dev/null +++ b/chushang-common/chushang-common-excel/pom.xml @@ -0,0 +1,24 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common-excel + + + + com.chushang + chushang-common-core + + + com.alibaba + easyexcel + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/DataListener.java b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/DataListener.java new file mode 100644 index 0000000..65fec84 --- /dev/null +++ b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/DataListener.java @@ -0,0 +1,30 @@ +package com.chushang.common.excel.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class DataListener extends AnalysisEventListener { + + /** + * 缓存的数据列表 + */ + private final List dataList = new ArrayList<>(); + + @Override + public void invoke(T data, AnalysisContext analysisContext) { + dataList.add(data); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + + } + +} \ No newline at end of file diff --git a/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/ImportListener.java b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/ImportListener.java new file mode 100644 index 0000000..33c66f9 --- /dev/null +++ b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/listener/ImportListener.java @@ -0,0 +1,72 @@ +package com.chushang.common.excel.listener; + +import cn.hutool.core.collection.CollectionUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.chushang.common.excel.service.ExcelImporter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +@Data +@Slf4j +@RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ImportListener extends AnalysisEventListener { + + /** + * 默认每隔3000条存储数据库 + */ + private int batchCount = 3000; + /** + * 缓存的数据列表 + */ + private List list = new ArrayList<>(); + /** + * 数据导入类 + */ + private final ExcelImporter importer; + + @Override + public void invoke(T data, AnalysisContext analysisContext) { + log.info("read size {}", list.size()); + if (isNotNull(data)) list.add(data); + // 达到BATCH_COUNT,则调用importer方法入库,防止数据几万条数据在内存,容易OOM + if (list.size() >= batchCount) { + // 调用importer方法 + importer.save(list); + // 存储完成清理list + list.clear(); + } + } + + + @SneakyThrows + private boolean isNotNull(T data){ + Class clazz = data.getClass(); + Field[] declaredFields = data.getClass().getDeclaredFields(); + List collect = new ArrayList<>(); + for (Field df : declaredFields) { + PropertyDescriptor pd = new PropertyDescriptor(df.getName(), clazz); + Object value = pd.getReadMethod().invoke(data); + collect.add(value); + } + return !CollectionUtil.hasNull(collect); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 调用importer方法 + importer.save(list); + // 存储完成清理list + list.clear(); + } + +} \ No newline at end of file diff --git a/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/service/ExcelImporter.java b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/service/ExcelImporter.java new file mode 100644 index 0000000..4b5a48f --- /dev/null +++ b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/service/ExcelImporter.java @@ -0,0 +1,8 @@ +package com.chushang.common.excel.service; + +import java.util.List; + +public interface ExcelImporter { + void save(List data); + +} diff --git a/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/utils/ExcelUtils.java b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/utils/ExcelUtils.java new file mode 100644 index 0000000..12aa256 --- /dev/null +++ b/chushang-common/chushang-common-excel/src/main/java/com/chushang/common/excel/utils/ExcelUtils.java @@ -0,0 +1,177 @@ +package com.chushang.common.excel.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.read.builder.ExcelReaderBuilder; +import com.alibaba.excel.read.listener.ReadListener; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.excel.listener.DataListener; +import com.chushang.common.excel.listener.ImportListener; +import com.chushang.common.excel.service.ExcelImporter; +import lombok.SneakyThrows; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * by zhaowenyuan create 2021/12/27 10:49 + * 表格工具 + */ +public class ExcelUtils { + /** + * 导出文件时为Writer生成OutputStream. + * + * @param response response + * @return "" + */ + public static OutputStream getOutputStream(HttpServletResponse response){ + try { + response.setContentType("application/vnd.ms-excel; charset=utf-8"); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/octet-stream;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=filename.xlsx"); + response.setHeader("Pragma", "public"); + response.setHeader("Cache-Control", "no-store"); + response.addHeader("Cache-Control", "max-age=0"); + return response.getOutputStream(); + } catch (IOException e) { + throw new ResultException("导出excel表格失败!", e); + } + } + + @SneakyThrows + public static void exportList(HttpServletResponse response, Class clazz, List list, String sheetName){ + OutputStream outputStream = getOutputStream(response); + EasyExcel.write(outputStream, clazz) + .sheet(sheetName) + .doWrite(list); + + outputStream.close(); + } + + + /** + * 读取excel的所有sheet数据 + * + * @param excel excel文件 + * @return List + */ + public static List read(MultipartFile excel, Class clazz) { + DataListener dataListener = new DataListener<>(); + ExcelReaderBuilder builder = getReaderBuilder(excel, dataListener, clazz); + if (builder == null) { + return null; + } + builder.doReadAll(); + return dataListener.getDataList(); + } + + /** + * 读取excel的指定sheet数据 + * + * @param excel excel文件 + * @param sheetNo sheet序号(从0开始) + * @return List + */ + public static List read(MultipartFile excel, int sheetNo, Class clazz) { + return read(excel, sheetNo, 1, clazz); + } + + /** + * 读取excel的指定sheet数据 + * + * @param excel excel文件 + * @param sheetNo sheet序号(从0开始) + * @param headRowNumber 表头行数 + * @return List + */ + public static List read(MultipartFile excel, int sheetNo, int headRowNumber, Class clazz) { + DataListener dataListener = new DataListener<>(); + ExcelReaderBuilder builder = getReaderBuilder(excel, dataListener, clazz); + if (builder == null) { + return null; + } + builder.sheet(sheetNo).headRowNumber(headRowNumber).doRead(); + return dataListener.getDataList(); + } + + public static List read(InputStream inputStream, Class clazz){ + DataListener dataListener = new DataListener<>(); + ExcelReaderBuilder builder = getReaderBuilder(inputStream, dataListener, clazz); + builder.doReadAll(); + return dataListener.getDataList(); + } + + /** + * 读取并导入数据 + * + * @param excel excel文件 + * @param importer 导入逻辑类 + * @param 泛型 + */ + public static void save(MultipartFile excel, ExcelImporter importer, Class clazz) { + ImportListener importListener = new ImportListener<>(importer); + ExcelReaderBuilder builder = getReaderBuilder(excel, importListener, clazz); + if (builder != null) { + builder.doReadAll(); + } + } + public static void save(List rowList, ExcelImporter importer) { + importer.save(rowList); + } + /** + * 导出excel + * + * @param response 响应类 + * @param fileName 文件名 + * @param sheetName sheet名 + * @param dataList 数据列表 + * @param clazz class类 + * @param 泛型 + */ + @SneakyThrows + public static void exportList(HttpServletResponse response, String fileName, String sheetName, List dataList, Class clazz) { + response.setContentType("application/vnd.ms-excel"); + response.setCharacterEncoding("UTF-8"); + fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); + response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); + exportList(response, clazz, dataList, sheetName); + } + + public static ExcelReaderBuilder getReaderBuilder(InputStream inputStream, ReadListener readListener, Class clazz) { + return EasyExcel.read(inputStream, clazz, readListener); + } + + /** + * 获取构建类 + * + * @param excel excel文件 + * @param readListener excel监听类 + * @return ExcelReaderBuilder + */ + public static ExcelReaderBuilder getReaderBuilder(MultipartFile excel, ReadListener readListener, Class clazz) { + String filename = excel.getOriginalFilename(); + if (StringUtils.isEmpty(filename)) { + throw new RuntimeException("请上传文件!"); + } + if ((!StringUtils.endsWithIgnoreCase(filename, ".xls") + && !StringUtils.endsWithIgnoreCase(filename, ".xlsx"))) { + throw new RuntimeException("请上传正确的excel文件!"); + } + InputStream inputStream; + try { + inputStream = new BufferedInputStream(excel.getInputStream()); + return getReaderBuilder(inputStream, readListener, clazz); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/chushang-common/chushang-common-excel/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-excel/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ab43764 --- /dev/null +++ b/chushang-common/chushang-common-excel/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration= diff --git a/chushang-common/chushang-common-feign/pom.xml b/chushang-common/chushang-common-feign/pom.xml new file mode 100644 index 0000000..3e60f82 --- /dev/null +++ b/chushang-common/chushang-common-feign/pom.xml @@ -0,0 +1,55 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common-feign + + + + com.chushang + chushang-common-core + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + com.alibaba + fastjson + + + + + + io.github.openfeign + feign-okhttp + + + + com.github.ben-manes.caffeine + caffeine + + + com.alibaba.csp + sentinel-datasource-nacos + + + com.alibaba + fastjson + + + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableOnnFeignClients.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableOnnFeignClients.java new file mode 100644 index 0000000..481e107 --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableOnnFeignClients.java @@ -0,0 +1,87 @@ +package com.chushang.common.feign.annotation;/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.chushang.common.feign.interceptor.FeignHeaderInterceptor; +import com.chushang.common.feign.interceptor.MdcInterceptor; +import com.chushang.common.feign.registrar.MdcFilterRegistrar; +import com.chushang.common.feign.registrar.TransferFeginFilterRegistrar; +import com.chushang.common.feign.registrar.TransferFilterRegistrar; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClientsConfiguration; +import org.springframework.cloud.openfeign.ChushangFeignClientsRegistrar; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * @author lengleng + * @date 2019/2/1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnableFeignClients +@Import({ + ChushangFeignClientsRegistrar.class +}) +public @interface EnableOnnFeignClients { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of + * {@code @ComponentScan(basePackages="org.my.pkg")}. + * @return the array of 'basePackages'. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. + *

+ * {@link #value()} is an alias for (and mutually exclusive with) this attribute. + *

+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based + * package names. + * @return the array of 'basePackages'. + */ + String[] basePackages() default { "com.chushang" }; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * @return the array of 'basePackageClasses'. + */ + Class[] basePackageClasses() default {}; + + /** + * A custom @Configuration for all feign clients. Can contain override + * @Bean definition for the pieces that make up the client, for instance + * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. + * + * @see FeignClientsConfiguration for the defaults + */ + Class[] defaultConfiguration() default {}; + + /** + * List of classes annotated with @FeignClient. If not empty, disables classpath + * scanning. + * @return + */ + Class[] clients() default {}; + +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableTransferFeign.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableTransferFeign.java new file mode 100644 index 0000000..cd6ee7a --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/annotation/EnableTransferFeign.java @@ -0,0 +1,33 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.annotation; + +import com.chushang.common.feign.interceptor.FeignHeaderInterceptor; +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; + +import com.chushang.common.feign.interceptor.MdcInterceptor; +import com.chushang.common.feign.registrar.MdcFilterRegistrar; +import com.chushang.common.feign.registrar.TransferFeginFilterRegistrar; +import com.chushang.common.feign.registrar.TransferFilterRegistrar; +import org.springframework.context.annotation.Import; + +/** + * 链路 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Import({FeignHeaderInterceptor.class, TransferFeginFilterRegistrar.class, TransferFilterRegistrar.class, MdcInterceptor.class, MdcFilterRegistrar.class}) +public @interface EnableTransferFeign { + /** + * 链路传递 参数 + */ + String[] attrName() default {}; +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/MdcFilter.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/MdcFilter.java new file mode 100644 index 0000000..17a758a --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/MdcFilter.java @@ -0,0 +1,48 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.filter; + +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import com.alibaba.fastjson2.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +public class MdcFilter implements Filter { + private static final Logger log = LoggerFactory.getLogger(MdcFilter.class); + public static final String TRECE_NAME = "trace"; + public static final String SPAN_NAME = "span"; + + public MdcFilter() { + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest)request; + String trace = req.getHeader("trace"); + String span = req.getHeader("span"); + String uri = req.getRequestURI(); + String userAgent = req.getHeader("user-agent"); + if (!uri.contains(".") || userAgent.startsWith("Java")) { + MDC.put("trace", Objects.requireNonNullElseGet(trace, () -> UUID.randomUUID().toString().replaceAll("-", ""))); + + MDC.put("span", Objects.requireNonNullElseGet(span, () -> UUID.randomUUID().toString().replaceAll("-", ""))); + + log.info("Request -->URL:{},Param:{}", uri, JSONObject.toJSONString(request.getParameterMap())); + } + + chain.doFilter(request, response); + MDC.clear(); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFeginFilter.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFeginFilter.java new file mode 100644 index 0000000..bf8006a --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFeginFilter.java @@ -0,0 +1,69 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.filter; + +import com.chushang.common.feign.interceptor.FeignHeaderInterceptor; +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import org.springframework.util.ObjectUtils; + +public class TransferFeginFilter implements Filter { + private String[] attrName; + + public TransferFeginFilter() { + } + + public TransferFeginFilter(String[] attrName) { + this.attrName = attrName; + } + + public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest)arg0; + String[] var5 = this.attrName; + for (String attr : var5) { + Object value = this.getAttr(req, attr); + if (!ObjectUtils.isEmpty(value)) { + FeignHeaderInterceptor.add(attr, value); + } + } + + arg2.doFilter(arg0, arg1); + } + + private Object getAttr(HttpServletRequest req, String key) { + Object value = req.getParameter(key); + if (!ObjectUtils.isEmpty(value)) { + return value; + } else { + Object attribute = req.getAttribute(key); + if (!ObjectUtils.isEmpty(attribute)) { + return attribute; + } else { + attribute = req.getHeader(key); + if (!ObjectUtils.isEmpty(attribute)) { + return attribute; + } else { + Cookie[] cs = req.getCookies(); + if (!ObjectUtils.isEmpty(cs)) { + for (Cookie c : cs) { + if (key.equals(c.getName())) { + attribute = c.getValue(); + return attribute; + } + } + } + return null; + } + } + } + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFilter.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFilter.java new file mode 100644 index 0000000..a697eec --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/filter/TransferFilter.java @@ -0,0 +1,63 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.filter; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import com.alibaba.fastjson2.JSONObject; +import org.springframework.util.ObjectUtils; + +public class TransferFilter implements Filter { + private static final ThreadLocal current = new ThreadLocal(); + private static final String charHax = "0123456789ABCDEF"; + + public TransferFilter() { + } + + public static T get(String key, Class type) { + JSONObject json = current.get(); + return json != null && json.containsKey(key) ? json.getObject(key, type) : null; + } + + public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest)arg0; + String json = req.getHeader("transfer"); + if (!ObjectUtils.isEmpty(json)) { + current.set(JSONObject.parseObject(new String(this.hexStringToBytes(json)))); + } + + arg2.doFilter(arg0, arg1); + current.remove(); + } + + public byte[] hexStringToBytes(String hexString) { + if (hexString != null && !hexString.equals("")) { + hexString = hexString.toUpperCase(); + int length = hexString.length() / 2; + char[] hexChars = hexString.toCharArray(); + byte[] d = new byte[length]; + + for(int i = 0; i < length; ++i) { + int pos = i * 2; + d[i] = (byte)(this.charToByte(hexChars[pos]) << 4 | this.charToByte(hexChars[pos + 1])); + } + + return d; + } else { + return null; + } + } + + private byte charToByte(char c) { + return (byte)"0123456789ABCDEF".indexOf(c); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/FeignHeaderInterceptor.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/FeignHeaderInterceptor.java new file mode 100644 index 0000000..04a4efe --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/FeignHeaderInterceptor.java @@ -0,0 +1,48 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.interceptor; + +import com.alibaba.fastjson2.JSONObject; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.springframework.core.annotation.Order; + +@Order(5) +public class FeignHeaderInterceptor implements RequestInterceptor { + public static final String attrKey = "transfer"; + private static final ThreadLocal header = ThreadLocal.withInitial(() -> new JSONObject()); + private static final byte[] hex = "0123456789ABCDEF".getBytes(); + + public FeignHeaderInterceptor() { + } + + public static void add(String key, Object value) { + header.get().put(key, value); + } + + public static void clear() { + header.remove(); + } + + public void apply(RequestTemplate template) { + if (header.get() != null && !header.get().isEmpty()) { + JSONObject h = header.get(); + template.header("transfer", this.bytes2HexString(h.toJSONString().getBytes())); + } + + } + + public String bytes2HexString(byte[] b) { + byte[] buff = new byte[2 * b.length]; + + for(int i = 0; i < b.length; ++i) { + buff[2 * i] = hex[b[i] >> 4 & 15]; + buff[2 * i + 1] = hex[b[i] & 15]; + } + + return new String(buff); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/MdcInterceptor.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/MdcInterceptor.java new file mode 100644 index 0000000..5d38f9f --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/interceptor/MdcInterceptor.java @@ -0,0 +1,20 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.interceptor; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.slf4j.MDC; + +public class MdcInterceptor implements RequestInterceptor { + public MdcInterceptor() { + } + + public void apply(RequestTemplate template) { + template.header("trace", new String[]{MDC.get("trace")}); + template.header("span", new String[]{MDC.get("span")}); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/MdcFilterRegistrar.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/MdcFilterRegistrar.java new file mode 100644 index 0000000..5be3a8e --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/MdcFilterRegistrar.java @@ -0,0 +1,37 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.registrar; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.Filter; + +import com.chushang.common.feign.filter.MdcFilter; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; + +public class MdcFilterRegistrar implements ImportBeanDefinitionRegistrar { + public MdcFilterRegistrar() { + } + + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + Filter filter = new MdcFilter(); + List patter = new ArrayList<>(); + patter.add("/*"); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(FilterRegistrationBean.class); + beanDefinitionBuilder.addPropertyValue("filter", filter); + beanDefinitionBuilder.addPropertyValue("order", 3); + beanDefinitionBuilder.addPropertyValue("urlPatterns", patter); + AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + beanDefinition.setBeanClass(FilterRegistrationBean.class); + beanDefinition.setScope("singleton"); + registry.registerBeanDefinition(filter.getClass().getName(), beanDefinition); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFeginFilterRegistrar.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFeginFilterRegistrar.java new file mode 100644 index 0000000..9ab394d --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFeginFilterRegistrar.java @@ -0,0 +1,41 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.registrar; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.Filter; + +import com.chushang.common.feign.annotation.EnableTransferFeign; +import com.chushang.common.feign.filter.TransferFeginFilter; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +public class TransferFeginFilterRegistrar implements ImportBeanDefinitionRegistrar { + public TransferFeginFilterRegistrar() { + } + + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + AnnotationAttributes attrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableTransferFeign.class.getName())); + String[] attrName = attrs.getStringArray("attrName"); + Filter filter = new TransferFeginFilter(attrName); + List patter = new ArrayList<>(); + patter.add("/*"); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(FilterRegistrationBean.class); + beanDefinitionBuilder.addPropertyValue("filter", filter); + beanDefinitionBuilder.addPropertyValue("order", 2); + beanDefinitionBuilder.addPropertyValue("urlPatterns", patter); + AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + beanDefinition.setBeanClass(FilterRegistrationBean.class); + beanDefinition.setScope("singleton"); + registry.registerBeanDefinition(filter.getClass().getName(), beanDefinition); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFilterRegistrar.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFilterRegistrar.java new file mode 100644 index 0000000..cb537fd --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/feign/registrar/TransferFilterRegistrar.java @@ -0,0 +1,38 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package com.chushang.common.feign.registrar; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.Filter; + +import com.chushang.common.feign.filter.TransferFilter; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; + +public class TransferFilterRegistrar implements ImportBeanDefinitionRegistrar { + public TransferFilterRegistrar() { + } + + public void registerBeanDefinitions(@NotNull AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + Filter filter = new TransferFilter(); + List patter = new ArrayList<>(); + patter.add("/*"); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(FilterRegistrationBean.class); + beanDefinitionBuilder.addPropertyValue("filter", filter); + beanDefinitionBuilder.addPropertyValue("order", 1); + beanDefinitionBuilder.addPropertyValue("urlPatterns", patter); + AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + beanDefinition.setBeanClass(FilterRegistrationBean.class); + beanDefinition.setScope("singleton"); + registry.registerBeanDefinition(filter.getClass().getName(), beanDefinition); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/SentinelAutoConfiguration.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/SentinelAutoConfiguration.java new file mode 100644 index 0000000..5586ea8 --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/SentinelAutoConfiguration.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.chushang.common.sentinel; + +import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.chushang.common.sentinel.handle.OnnUrlBlockHandler; +import com.chushang.common.sentinel.ext.OnnSentinelFeign; +import com.chushang.common.sentinel.ext.OnnSentinelFilterConfiguration; +import com.chushang.common.sentinel.parser.OnnHeaderRequestOriginParser; +import feign.Feign; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +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.context.annotation.Import; +import org.springframework.context.annotation.Scope; + +/** + * @author lengleng + * @date 2020-02-12 + *

+ * sentinel 配置 + */ +@Configuration(proxyBeanMethods = false) +@Import(OnnSentinelFilterConfiguration.class) +@AutoConfigureBefore(SentinelFeignAutoConfiguration.class) +public class SentinelAutoConfiguration { + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "feign.sentinel.enabled") + public Feign.Builder feignSentinelBuilder() { + return OnnSentinelFeign.builder(); + } + + @Bean + @ConditionalOnMissingBean + public BlockExceptionHandler blockExceptionHandler() { + return new OnnUrlBlockHandler(); + } + + @Bean + @ConditionalOnMissingBean + public RequestOriginParser requestOriginParser() { + return new OnnHeaderRequestOriginParser(); + } + +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFeign.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFeign.java new file mode 100644 index 0000000..dc5c17d --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFeign.java @@ -0,0 +1,129 @@ +package com.chushang.common.sentinel.ext; +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; +import feign.Contract; +import feign.Feign; +import feign.InvocationHandlerFactory; +import feign.Target; +import org.springframework.beans.BeansException; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * 支持自动降级注入 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelFeign} + * + * @author lengleng + * @date 2020/6/9 + */ +public final class OnnSentinelFeign { + + private OnnSentinelFeign() { + + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends Feign.Builder implements ApplicationContextAware { + + private Contract contract = new Contract.Default(); + + private FeignContext feignContext; + + @Override + public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { + throw new UnsupportedOperationException(); + } + + @Override + public Builder contract(Contract contract) { + this.contract = contract; + return this; + } + + @Override + public Feign build() { + super.invocationHandlerFactory(new InvocationHandlerFactory() { + @Override + public InvocationHandler create(Target target, Map dispatch) { + + // 查找 FeignClient 上的 降级策略 + FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class); + assert feignClient != null; + Class fallback = feignClient.fallback(); + Class fallbackFactory = feignClient.fallbackFactory(); + + String beanName = feignClient.contextId(); + if (!StringUtils.hasText(beanName)) { + beanName = feignClient.name(); + } + + Object fallbackInstance; + FallbackFactory fallbackFactoryInstance; + if (void.class != fallback) { + fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type()); + return new OnnSentinelInvocationHandler(target, dispatch, + new FallbackFactory.Default<>(fallbackInstance)); + } + + if (void.class != fallbackFactory) { + fallbackFactoryInstance = (FallbackFactory) getFromContext(beanName, "fallbackFactory", + fallbackFactory, FallbackFactory.class); + return new OnnSentinelInvocationHandler(target, dispatch, fallbackFactoryInstance); + } + return new OnnSentinelInvocationHandler(target, dispatch); + } + + private Object getFromContext(String name, String type, Class fallbackType, Class targetType) { + Object fallbackInstance = feignContext.getInstance(name, fallbackType); + if (fallbackInstance == null) { + throw new IllegalStateException(String.format( + "No %s instance of type %s found for feign client %s", type, fallbackType, name)); + } + + if (!targetType.isAssignableFrom(fallbackType)) { + throw new IllegalStateException(String.format( + "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", + type, fallbackType, targetType, name)); + } + return fallbackInstance; + } + }); + + super.contract(new SentinelContractHolder(contract)); + return super.build(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + feignContext = applicationContext.getBean(FeignContext.class); + } + + } + +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFilterConfiguration.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFilterConfiguration.java new file mode 100644 index 0000000..e075eed --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelFilterConfiguration.java @@ -0,0 +1,58 @@ +package com.chushang.common.sentinel.ext; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +/** + * @author lengleng + * @date 2021/12/4 + * + * 避免 spring cloud 2021 不兼容 的问题 + */ +@RequiredArgsConstructor +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class OnnSentinelFilterConfiguration { + + @Bean + public SentinelWebInterceptor sentinelWebInterceptor(SentinelWebMvcConfig sentinelWebMvcConfig) { + return new SentinelWebInterceptor(sentinelWebMvcConfig); + } + + @Bean + public SentinelWebMvcConfig sentinelWebMvcConfig(SentinelProperties properties, + Optional urlCleanerOptional, Optional blockExceptionHandlerOptional, + Optional requestOriginParserOptional) { + SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig(); + sentinelWebMvcConfig.setHttpMethodSpecify(properties.getHttpMethodSpecify()); + sentinelWebMvcConfig.setWebContextUnify(properties.getWebContextUnify()); + + if (blockExceptionHandlerOptional.isPresent()) { + blockExceptionHandlerOptional.ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler); + } + else { + if (StringUtils.hasText(properties.getBlockPage())) { + sentinelWebMvcConfig.setBlockExceptionHandler( + ((request, response, e) -> response.sendRedirect(properties.getBlockPage()))); + } + else { + sentinelWebMvcConfig.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); + } + } + + urlCleanerOptional.ifPresent(sentinelWebMvcConfig::setUrlCleaner); + requestOriginParserOptional.ifPresent(sentinelWebMvcConfig::setOriginParser); + return sentinelWebMvcConfig; + } + +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelInvocationHandler.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelInvocationHandler.java new file mode 100644 index 0000000..45c1f6e --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/ext/OnnSentinelInvocationHandler.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.sentinel.ext; + +import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; +import com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.chushang.common.core.web.Result; +import feign.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 支持自动降级注入 重写 {@link SentinelInvocationHandler} + * + * @author lengleng + * @date 2020/6/9 + */ +@Slf4j +public class OnnSentinelInvocationHandler implements InvocationHandler { + + public static final String EQUALS = "equals"; + + public static final String HASH_CODE = "hashCode"; + + public static final String TO_STRING = "toString"; + + private final Target target; + + private final Map dispatch; + + private FallbackFactory fallbackFactory; + + private Map fallbackMethodMap; + + OnnSentinelInvocationHandler(Target target, Map dispatch, + FallbackFactory fallbackFactory) { + this.target = Util.checkNotNull(target, "target"); + this.dispatch = Util.checkNotNull(dispatch, "dispatch"); + this.fallbackFactory = fallbackFactory; + this.fallbackMethodMap = toFallbackMethod(dispatch); + } + + OnnSentinelInvocationHandler(Target target, Map dispatch) { + this.target = Util.checkNotNull(target, "target"); + this.dispatch = Util.checkNotNull(dispatch, "dispatch"); + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + switch (method.getName()) { + case EQUALS: + try { + Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; + return equals(otherHandler); + } catch (IllegalArgumentException e) { + return false; + } + case HASH_CODE: + return hashCode(); + case TO_STRING: + return toString(); + } + + Object result; + InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method); + // only handle by HardCodedTarget + if (target instanceof Target.HardCodedTarget) { + Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target; + MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP + .get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method)); + // resource default is HttpMethod:protocol://url + if (methodMetadata == null) { + result = methodHandler.invoke(args); + } + else { + String resourceName = methodMetadata.template().method().toUpperCase() + ':' + hardCodedTarget.url() + + methodMetadata.template().path(); + log.info("resourceName : {}", resourceName); + Entry entry = null; + try { + ContextUtil.enter(resourceName); + entry = SphU.entry(resourceName, EntryType.OUT, 1, args); + result = methodHandler.invoke(args); + } + catch (Throwable ex) { + // fallback handle + if (!BlockException.isBlockException(ex)) { + Tracer.trace(ex); + } + if (fallbackFactory != null) { + try { + return fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex), args); + } + catch (IllegalAccessException e) { + // shouldn't happen as method is public due to being an + // interface + throw new AssertionError(e); + } + catch (InvocationTargetException e) { + throw new AssertionError(e.getCause()); + } + } + else { + // 若是R类型 执行自动降级返回R + if (method.getReturnType() == Result.class) { + log.error("feign 服务间调用异常", ex); + return Result.failed(ex.getLocalizedMessage()); + } + else { + throw ex; + } + } + } + finally { + if (entry != null) { + entry.exit(1, args); + } + ContextUtil.exit(); + } + } + } + else { + // other target type using default strategy + result = methodHandler.invoke(args); + } + + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SentinelInvocationHandler) { + OnnSentinelInvocationHandler other = (OnnSentinelInvocationHandler) obj; + return target.equals(other.target); + } + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } + + static Map toFallbackMethod(Map dispatch) { + Map result = new LinkedHashMap<>(); + for (Method method : dispatch.keySet()) { + method.setAccessible(true); + result.put(method, method); + } + return result; + } + +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/FeignExceptionHandler.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/FeignExceptionHandler.java new file mode 100644 index 0000000..bdc9819 --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/FeignExceptionHandler.java @@ -0,0 +1,20 @@ +package com.chushang.common.sentinel.handle; + +import com.chushang.common.core.web.Result; +import feign.FeignException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * by zhaowenyuan create 2022/4/7 10:00 + * FeignException$BadGateway + */ +@Slf4j +@RestControllerAdvice +public class FeignExceptionHandler { + @org.springframework.web.bind.annotation.ExceptionHandler(FeignException.class) + public Result handlerFeignException(FeignException e){ + log.error("",e); + return Result.failed(502, e.getMessage()); + } +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/OnnUrlBlockHandler.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/OnnUrlBlockHandler.java new file mode 100644 index 0000000..087599d --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/handle/OnnUrlBlockHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.sentinel.handle; + +import cn.hutool.http.ContentType; +import cn.hutool.json.JSONUtil; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.chushang.common.core.web.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * sentinel统一降级限流策略 + *

+ * {@link com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler} + * + * @author lengleng + * @date 2020-06-11 + */ +@Slf4j +public class OnnUrlBlockHandler implements BlockExceptionHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { + log.error("sentinel 降级 资源名称{}", e.getRule().getResource(), e); + + response.setContentType(ContentType.JSON.toString()); + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.getWriter().print(JSONUtil.toJsonStr(Result.failed(e.getMessage()))); + } + +} diff --git a/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/parser/OnnHeaderRequestOriginParser.java b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/parser/OnnHeaderRequestOriginParser.java new file mode 100644 index 0000000..2a4e41c --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/com/chushang/common/sentinel/parser/OnnHeaderRequestOriginParser.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.sentinel.parser; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; + +import javax.servlet.http.HttpServletRequest; + +/** + * sentinel 请求头解析判断 + * + * @author lengleng + * @date 2020-06-11 + */ +public class OnnHeaderRequestOriginParser implements RequestOriginParser { + + /** + * 请求头获取allow + */ + private static final String ALLOW = "Allow"; + + /** + * Parse the origin from given HTTP request. + * @param request HTTP request + * @return parsed origin + */ + @Override + public String parseOrigin(HttpServletRequest request) { + return request.getHeader(ALLOW); + } + +} diff --git a/chushang-common/chushang-common-feign/src/main/java/org/springframework/cloud/openfeign/ChushangFeignClientsRegistrar.java b/chushang-common/chushang-common-feign/src/main/java/org/springframework/cloud/openfeign/ChushangFeignClientsRegistrar.java new file mode 100644 index 0000000..6960cf2 --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/java/org/springframework/cloud/openfeign/ChushangFeignClientsRegistrar.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package org.springframework.cloud.openfeign; + +import com.chushang.common.sentinel.SentinelAutoConfiguration; +import lombok.Getter; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; + +/** + * @author L.cm + * @date 2020/2/8 + *

+ * feign 自动配置功能 from mica + */ +public class ChushangFeignClientsRegistrar implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware, EnvironmentAware { + + @Getter + private ClassLoader beanClassLoader; + + @Getter + private Environment environment; + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + registerFeignClients(registry); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + private void registerFeignClients(BeanDefinitionRegistry registry) { + List feignClients = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), + getBeanClassLoader()); + // 如果 spring.factories 里为空 + if (feignClients.isEmpty()) { + return; + } + for (String className : feignClients) { + try { + Class clazz = beanClassLoader.loadClass(className); + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, + FeignClient.class); + if (attributes == null) { + continue; + } + // 如果已经存在该 bean,支持原生的 Feign + if (registry.containsBeanDefinition(className)) { + continue; + } + registerClientConfiguration(registry, getClientName(attributes), attributes.get("configuration")); + + validate(attributes); + BeanDefinitionBuilder definition = BeanDefinitionBuilder + .genericBeanDefinition(FeignClientFactoryBean.class); + definition.addPropertyValue("url", getUrl(attributes)); + definition.addPropertyValue("path", getPath(attributes)); + String name = getName(attributes); + definition.addPropertyValue("name", name); + + // 兼容最新版本的 spring-cloud-openfeign,尚未发布 + StringBuilder aliasBuilder = new StringBuilder(18); + if (attributes.containsKey("contextId")) { + String contextId = getContextId(attributes); + aliasBuilder.append(contextId); + definition.addPropertyValue("contextId", contextId); + } + else { + aliasBuilder.append(name); + } + + definition.addPropertyValue("type", className); + definition.addPropertyValue("decode404", attributes.get("decode404")); + definition.addPropertyValue("fallback", attributes.get("fallback")); + definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + + AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); + + // alias + String alias = aliasBuilder.append("FeignClient").toString(); + + // has a default, won't be null + boolean primary = (Boolean) attributes.get("primary"); + + beanDefinition.setPrimary(primary); + + String qualifier = getQualifier(attributes); + if (StringUtils.hasText(qualifier)) { + alias = qualifier; + } + + BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, + new String[] { alias }); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + + } + catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + } + + /** + * Return the class used by {@link SpringFactoriesLoader} to load configuration + * candidates. + * @return the factory class + */ + private Class getSpringFactoriesLoaderFactoryClass() { + return SentinelAutoConfiguration.class; + } + + private void validate(Map attributes) { + AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes); + // This blows up if an aliased property is overspecified + FeignClientsRegistrar.validateFallback(annotation.getClass("fallback")); + FeignClientsRegistrar.validateFallbackFactory(annotation.getClass("fallbackFactory")); + } + + private String getName(Map attributes) { + String name = (String) attributes.get("serviceId"); + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("name"); + } + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("value"); + } + name = resolve(name); + return FeignClientsRegistrar.getName(name); + } + + private String getContextId(Map attributes) { + String contextId = (String) attributes.get("contextId"); + if (!StringUtils.hasText(contextId)) { + return getName(attributes); + } + + contextId = resolve(contextId); + return FeignClientsRegistrar.getName(contextId); + } + + private String resolve(String value) { + if (StringUtils.hasText(value)) { + return this.environment.resolvePlaceholders(value); + } + return value; + } + + private String getUrl(Map attributes) { + String url = resolve((String) attributes.get("url")); + return FeignClientsRegistrar.getUrl(url); + } + + private String getPath(Map attributes) { + String path = resolve((String) attributes.get("path")); + return FeignClientsRegistrar.getPath(path); + } + + @Nullable + private String getQualifier(@Nullable Map client) { + if (client == null) { + return null; + } + String qualifier = (String) client.get("qualifier"); + if (StringUtils.hasText(qualifier)) { + return qualifier; + } + return null; + } + + @Nullable + private String getClientName(@Nullable Map client) { + if (client == null) { + return null; + } + String value = (String) client.get("contextId"); + if (!StringUtils.hasText(value)) { + value = (String) client.get("value"); + } + if (!StringUtils.hasText(value)) { + value = (String) client.get("name"); + } + if (!StringUtils.hasText(value)) { + value = (String) client.get("serviceId"); + } + if (StringUtils.hasText(value)) { + return value; + } + + throw new IllegalStateException( + "Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName()); + } + + private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class); + builder.addConstructorArgValue(name); + builder.addConstructorArgValue(configuration); + registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), + builder.getBeanDefinition()); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + +} diff --git a/chushang-common/chushang-common-feign/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-feign/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b5387b2 --- /dev/null +++ b/chushang-common/chushang-common-feign/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.common.sentinel.SentinelAutoConfiguration diff --git a/chushang-common/chushang-common-job/pom.xml b/chushang-common/chushang-common-job/pom.xml new file mode 100644 index 0000000..7795d7b --- /dev/null +++ b/chushang-common/chushang-common-job/pom.xml @@ -0,0 +1,33 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common-job + + + 17 + 17 + UTF-8 + + + + 调度任务通用配置信息 + + + + com.xuxueli + xxl-job-core + + + org.springframework.cloud + spring-cloud-commons + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-job/src/main/java/com/chushang/job/core/config/XxlJobConfig.java b/chushang-common/chushang-common-job/src/main/java/com/chushang/job/core/config/XxlJobConfig.java new file mode 100644 index 0000000..778d291 --- /dev/null +++ b/chushang-common/chushang-common-job/src/main/java/com/chushang/job/core/config/XxlJobConfig.java @@ -0,0 +1,133 @@ +package com.chushang.job.core.config; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import io.netty.util.internal.StringUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Enumeration; + + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ +@Data +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "xxl.job") +public class XxlJobConfig { + + private String adminAddresses; + + private String accessToken; + + private Executor executor; + + @Data + public static class Executor { + private String appName; + + private String address; + + private String ip; + + private int port; + + private String logPath = "xxl-job/jobhandler"; + + private int logRetentionDays = 30; + } + + @Bean + public XxlJobSpringExecutor xxlJobExecutor() { + String ip = executor.ip; + if (StringUtil.isNullOrEmpty(ip)){ + ip = getServerIp(); + } + log.info(">>>>>>>>>>> xxl-job config init."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(adminAddresses); + xxlJobSpringExecutor.setAccessToken(accessToken); + + xxlJobSpringExecutor.setAppname(executor.appName); + xxlJobSpringExecutor.setAddress(executor.address); + + xxlJobSpringExecutor.setIp(ip); + xxlJobSpringExecutor.setPort(executor.port); + + xxlJobSpringExecutor.setLogPath(executor.logPath); + xxlJobSpringExecutor.setLogRetentionDays(executor.logRetentionDays); + return xxlJobSpringExecutor; + } + + /** + * 获取本地IP地址 + */ + public static String getServerIp() { + if (isWindowsOs()) { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + return ""; + } + } else { + return getLinuxLocalIp(); + } + } + + /** + * 判断操作系统是否是Windows + */ + public static boolean isWindowsOs() { + boolean isWindowsOs = false; + String osName = System.getProperty("os.name"); + if ("WINDOWS".contains(osName.toLowerCase())) { + isWindowsOs = true; + } + return isWindowsOs; + } + + /** + * 获取Linux下的IP地址 + * + * @return IP地址 + */ + private static String getLinuxLocalIp() { + String ip = ""; + try { + for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { + NetworkInterface networkInterface = en.nextElement(); + String name = networkInterface.getName(); + if (!name.contains("docker") && !name.contains("lo")) { + for (Enumeration enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (!inetAddress.isLoopbackAddress()) { + String ipaddress = inetAddress.getHostAddress(); + if (!ipaddress.contains("::") && !ipaddress.contains("0:0:") && !ipaddress.contains("fe80")) { + ip = ipaddress; + } + } + } + } + } + } catch (SocketException ex) { + log.error("获取ip地址异常", ex); + } + return ip; + } + +} \ No newline at end of file diff --git a/chushang-common/chushang-common-job/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-job/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..413b417 --- /dev/null +++ b/chushang-common/chushang-common-job/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.job.core.config.XxlJobConfig diff --git a/chushang-common/chushang-common-log/pom.xml b/chushang-common/chushang-common-log/pom.xml new file mode 100644 index 0000000..3d1acf0 --- /dev/null +++ b/chushang-common/chushang-common-log/pom.xml @@ -0,0 +1,34 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + 1.0.0 + chushang-common-log + + + 17 + 17 + + + + + com.chushang + chushang-common-core + + + com.chushang + chushang-common-mybatis + + + com.chushang + chushang-common-security + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/annotation/SysLog.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/annotation/SysLog.java new file mode 100644 index 0000000..30affd8 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/annotation/SysLog.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2016-2019 人人开源 All rights reserved. + * + * https://www.renren.io + * + * 版权所有,侵权必究! + */ + +package com.chushang.common.log.annotation; + +import java.lang.annotation.*; + +/** + * 系统日志注解 + * + * @author Mark sunlightcs@gmail.com + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SysLog { + + String value() default ""; + + /** + * 判断参数下标 -- 需要与 参数下标一一对应 + * -2 代表删除 + * -1 代表默认 + * 0 - Integer.MAX_VALUE 代表 参数下标 + */ + int index() default -1; + + boolean export() default false; + +} diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/aspect/SysLogAspect.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/aspect/SysLogAspect.java new file mode 100644 index 0000000..4970c51 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/aspect/SysLogAspect.java @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2016-2019 人人开源 All rights reserved. + * + * https://www.renren.io + * + * 版权所有,侵权必究! + */ + +package com.chushang.common.log.aspect; + +import cn.hutool.core.util.URLUtil; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.jackson.JacksonUtils; +import com.chushang.common.core.util.IPUtils; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.log.annotation.SysLog; +import com.chushang.common.log.entity.SysLogEntity; +import com.chushang.common.log.enums.LogTypeEnum; +import com.chushang.common.log.service.SysLogService; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.vo.LoginUser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * 系统日志,切面处理类 + * + * @author Mark sunlightcs@gmail.com + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class SysLogAspect { + + final SysLogService sysLogService; + + @Pointcut("@annotation(com.chushang.common.log.annotation.SysLog)") + public void logPointCut() { + } + + /** + * 不主动拦截get 请求 + */ + @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)" + + "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" + + "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" + + "|| @annotation(org.springframework.web.bind.annotation.PutMapping)") + public void exceptionLogPointCut() { + } + + @AfterReturning("logPointCut()") + public void around(JoinPoint point) { + long beginTime = System.currentTimeMillis(); + //执行时长(毫秒) + long time = System.currentTimeMillis() - beginTime; + //保存日志 + saveSysLog(point, time, LogTypeEnum.NORMAL, null); + } + + /** + * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行 + * --> 未执行, 因为 有全局捕捉一长 + * @param joinPoint 切入点 + * @param ex 异常信息 + */ + @AfterThrowing(pointcut = "exceptionLogPointCut()", throwing = "ex") + public void saveExceptionLog(JoinPoint joinPoint, Throwable ex) + { + long beginTime = System.currentTimeMillis(); + //执行时长(毫秒) + long time = System.currentTimeMillis() - beginTime; + + saveSysLog(joinPoint, time, LogTypeEnum.ERROR, ex.getMessage()); + } + + private void saveSysLog(JoinPoint joinPoint, long time, LogTypeEnum type, String ex) { + try { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + // 获取到的所有参数 + Object[] args = joinPoint.getArgs(); + + + SysLogEntity sysLogEntity = new SysLogEntity(); + SysLog syslog = + method.getAnnotation(SysLog.class); + + if(syslog != null){ + String value = syslog.value(); + int index = syslog.index(); + if (index != -1){ + if (index >= 0){ + String id = args[index] + ""; + if ("0".equals(id)){ + value += "新增 "; + }else { + value += "修改 "; + } + }else if (index == -2){ + value += "删除 "; + } + } + sysLogEntity.setOperation(value); + if (null != args && args.length > 0){ + // 将导出的response 过滤掉 + List argList = + Arrays.stream(args) + .filter(arg -> !(arg instanceof HttpServletResponse)) + .collect(Collectors.toList()); + sysLogEntity.setParams(JacksonUtils.toJSONString(argList)); + } + } + + //获取request + HttpServletRequest request = ServletUtils.getRequest(); + String ipAddr = IPUtils.clientIp(request); + //请求的方法名 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = signature.getName(); + sysLogEntity.setMethod(className + "." + methodName + "()"); + //设置IP地址 + sysLogEntity.setRemoteAddr(ipAddr); + // 请求uri + sysLogEntity.setRequestUri(URLUtil.getPath(request.getRequestURI())); + // 请求代理 + sysLogEntity.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT)); + + LoginUser loginUser = SecurityUtils.getLoginUser(); + // 请求账号信息 + if (null != loginUser){ + Integer userId = loginUser.getUserId(); + //用户名 + sysLogEntity.setUserId(userId); + String username = loginUser.getUsername(); + sysLogEntity.setUsername(username); + //保存系统日志 + } + else { + if (!ipAddr.equals("localhost") && !ipAddr.equals("127.0.0.1")){ + throw new ResultException("没有权限"); + } + sysLogEntity.setUserId(1); + sysLogEntity.setUsername("error"); + } + // 执行时间 + sysLogEntity.setTime(time); + // 日志类型 + sysLogEntity.setType(type); + sysLogEntity.setException(ex); + sysLogService.save(sysLogEntity); + }catch (Exception e){ + HttpServletRequest request = ServletUtils.getRequest(); + // 保存日志失败 不应当影响 正常的操作 + log.error("uri : {}, save log error",URLUtil.getPath(request.getRequestURI()),e); + } + } + + +} diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/controller/SysLogController.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/controller/SysLogController.java new file mode 100644 index 0000000..65d5004 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/controller/SysLogController.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2016-2019 人人开源 All rights reserved. + * + * https://www.renren.io + * + * 版权所有,侵权必究! + */ + +package com.chushang.common.log.controller; + +import com.chushang.common.log.entity.dto.ListLogDTO; +import com.chushang.common.core.web.Result; +import com.chushang.common.log.service.SysLogService; +import com.chushang.common.mybatis.utils.PageUtils; +import com.chushang.security.annotation.RequiresPermissions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + + +/** + * 系统日志 + * + * @author Mark sunlightcs@gmail.com + */ +@Controller +@RequestMapping("/v1/log") +public class SysLogController { + @Autowired + private SysLogService sysLogService; + + /** + * 列表 + */ + @ResponseBody + @GetMapping("/list") + @RequiresPermissions("sys:log:list") + public Result list(ListLogDTO params){ + PageUtils page = sysLogService.queryPage(params); + return Result.ok(page); + } + +} diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/SysLogEntity.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/SysLogEntity.java new file mode 100644 index 0000000..c99bf0e --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/SysLogEntity.java @@ -0,0 +1,106 @@ +package com.chushang.common.log.entity; + +import cn.hutool.core.date.DatePattern; +import com.baomidou.mybatisplus.annotation.*; +import com.chushang.common.log.enums.LogTypeEnum; +import com.chushang.common.mybatis.base.BaseEntity; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 系统日志 + */ +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "sys_log") +public class SysLogEntity { + /** + * 主键id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 日志类型 + */ + @TableField(value = "type") + private LogTypeEnum type; + /** + * 操作ip 地址 + */ + @TableField(value = "remote_addr") + private String remoteAddr; + /** + * 用户代理 + */ + @TableField(value = "user_agent") + private String userAgent; + /** + * 请求URI + */ + @TableField(value = "request_uri") + private String requestUri; + /** + * 请求方法 + */ + @TableField(value = "method") + private String method; + /** + * 请求参数 + */ + @TableField(value = "params") + private String params; + /** + * 执行时间 毫秒 + */ + @TableField(value = "time") + private Long time; + /** + * 异常信息 + */ + @TableField(value = "exception") + private String exception; + /** + * 操作用户id + */ + @TableField(value = "user_id") + private Integer userId; + /** + * 操作用户账号 + */ + @TableField(value = "username") + private String username; + /** + * 用户操作 + */ + @TableField(value = "operation") + private String operation; + + /** + * 创建时间 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @TableField(value = "create_time",fill = FieldFill.INSERT, updateStrategy = FieldStrategy.NEVER) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime createTime; + /** + * 更新时间 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE, updateStrategy = FieldStrategy.NOT_NULL) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime updateTime; +} \ No newline at end of file diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/dto/ListLogDTO.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/dto/ListLogDTO.java new file mode 100644 index 0000000..117192c --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/entity/dto/ListLogDTO.java @@ -0,0 +1,16 @@ +package com.chushang.common.log.entity.dto; + +import com.chushang.common.core.util.CommonParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * by zhaowenyuan create 2022/5/20 19:09 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class ListLogDTO extends CommonParam { + + private String title; + +} diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/enums/LogTypeEnum.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/enums/LogTypeEnum.java new file mode 100644 index 0000000..01a0fe7 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/enums/LogTypeEnum.java @@ -0,0 +1,29 @@ +package com.chushang.common.log.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; +import com.chushang.common.core.web.EnumUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; +/** + * by zhaowenyuan create 2022/3/16 14:48 + */ +@Getter +@AllArgsConstructor +public enum LogTypeEnum implements EnumUtils.CodeEnum, IEnum { + NORMAL(0,"正常日志"), + ERROR(9,"错误日志") + ; + private final Integer code; + private final String desc; + + + @Override + public Integer getValue() { + return this.code; + } + + @Override + public String getMsg() { + return this.desc; + } +} diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/mapper/SysLogMapper.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/mapper/SysLogMapper.java new file mode 100644 index 0000000..80bf0c4 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/mapper/SysLogMapper.java @@ -0,0 +1,8 @@ +package com.chushang.common.log.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.common.log.entity.SysLogEntity; + +public interface SysLogMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/SysLogService.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/SysLogService.java new file mode 100644 index 0000000..3bab416 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/SysLogService.java @@ -0,0 +1,30 @@ +package com.chushang.common.log.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.common.log.entity.SysLogEntity; +import com.chushang.common.log.entity.dto.ListLogDTO; +import com.chushang.common.mybatis.utils.PageUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * by zhaowenyuan create 2022/3/16 15:35 + */ +public interface SysLogService extends IService { + + default PageUtils queryPage(ListLogDTO params){ + String key = params.getTitle(); + + IPage page = this.page( + new Page<>(params.getPage(), params.getLimit()), + new LambdaQueryWrapper() + .like(StringUtils.isNotBlank(key),SysLogEntity::getUsername, key) + .or() + .like(StringUtils.isNotEmpty(key), SysLogEntity::getOperation, key) + .orderByDesc(SysLogEntity::getCreateTime) + ); + return new PageUtils(page); + } +} diff --git a/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/impl/SysLogServiceImpl.java b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/impl/SysLogServiceImpl.java new file mode 100644 index 0000000..270a6f3 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/java/com/chushang/common/log/service/impl/SysLogServiceImpl.java @@ -0,0 +1,16 @@ +package com.chushang.common.log.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.common.log.entity.SysLogEntity; +import com.chushang.common.log.mapper.SysLogMapper; +import com.chushang.common.log.service.SysLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * by zhaowenyuan create 2022/3/16 15:35 + */ +@Slf4j +@Service +public class SysLogServiceImpl extends ServiceImpl implements SysLogService { +} diff --git a/chushang-common/chushang-common-log/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-log/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ab43764 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration= diff --git a/chushang-common/chushang-common-log/src/main/resources/mapper/SysLogMapper.xml b/chushang-common/chushang-common-log/src/main/resources/mapper/SysLogMapper.xml new file mode 100644 index 0000000..a7f7c85 --- /dev/null +++ b/chushang-common/chushang-common-log/src/main/resources/mapper/SysLogMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-mail/pom.xml b/chushang-common/chushang-common-mail/pom.xml new file mode 100644 index 0000000..89db179 --- /dev/null +++ b/chushang-common/chushang-common-mail/pom.xml @@ -0,0 +1,36 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common-mail + + + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-mail + + + com.chushang + chushang-common-core + + + org.springframework.boot + spring-boot-starter-web + + + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-mail/src/main/java/com/chushang/common/mail/utils/MailUtils.java b/chushang-common/chushang-common-mail/src/main/java/com/chushang/common/mail/utils/MailUtils.java new file mode 100644 index 0000000..14cebe6 --- /dev/null +++ b/chushang-common/chushang-common-mail/src/main/java/com/chushang/common/mail/utils/MailUtils.java @@ -0,0 +1,43 @@ +package com.chushang.common.mail.utils; + +import com.chushang.common.core.util.SpringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +@Slf4j +@Component +public class MailUtils { + + private static final JavaMailSender javaMailSender = SpringUtils.getBean(JavaMailSender.class); + + private static String from; + @Value("${spring.mail.username}") + public void setFrom(String from) { + MailUtils.from = from; + } + + public static void send(String from, String[] to, String subject, String content, Boolean isHtml) throws MessagingException { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); + helper.setFrom(from); + helper.setTo(to); + helper.setText(content, isHtml); + helper.setSubject(subject); + javaMailSender.send(mimeMessage); + } + + public static void sendText(String[] to, String subject, String content) throws MessagingException { + send(from, to, subject, content, false); + } + + public static void sendHtml(String[] to, String subject, String content) throws MessagingException { + send(from, to, subject, content, true); + } + +} \ No newline at end of file diff --git a/chushang-common/chushang-common-mail/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-mail/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b5cf8ae --- /dev/null +++ b/chushang-common/chushang-common-mail/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.common.mail.utils.MailUtils diff --git a/chushang-common/chushang-common-mongo/README.md b/chushang-common/chushang-common-mongo/README.md new file mode 100644 index 0000000..cf58433 --- /dev/null +++ b/chushang-common/chushang-common-mongo/README.md @@ -0,0 +1,2 @@ +mongo 文档 +https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#preface \ No newline at end of file diff --git a/chushang-common/chushang-common-mongo/pom.xml b/chushang-common/chushang-common-mongo/pom.xml new file mode 100644 index 0000000..e2313e0 --- /dev/null +++ b/chushang-common/chushang-common-mongo/pom.xml @@ -0,0 +1,25 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common-mongo + + + 17 + 17 + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/EnableMongo.java b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/EnableMongo.java new file mode 100644 index 0000000..d57c8bb --- /dev/null +++ b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/EnableMongo.java @@ -0,0 +1,17 @@ +package com.chushang.common.mongo; + +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import java.lang.annotation.*; + +/** + * @author by zhaowenyuan create 2022/7/18 14:41 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnableMongoRepositories +@EnableTransactionManagement +public @interface EnableMongo { +} diff --git a/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/annotation/MongoDel.java b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/annotation/MongoDel.java new file mode 100644 index 0000000..430eda2 --- /dev/null +++ b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/annotation/MongoDel.java @@ -0,0 +1,15 @@ +package com.chushang.common.mongo.annotation; + +import java.lang.annotation.*; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +public @interface MongoDel { + /** + * 是否逻辑删除 + * true 逻辑删除, false 直接删除 + */ + boolean logic() default false; + +} diff --git a/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/config/MongoTransaction.java b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/config/MongoTransaction.java new file mode 100644 index 0000000..6c8a594 --- /dev/null +++ b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/config/MongoTransaction.java @@ -0,0 +1,20 @@ +package com.chushang.common.mongo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.MongoTransactionManager; + +/** + * @author by zhaowenyuan create 2022/7/18 14:34 + */ +public class MongoTransaction { + /** + * 配置 Mongo 事务管理 + * 需要 mongo 数据库开启 副本 + * 单机情况下, 事务无效 + */ + @Bean + public MongoTransactionManager transactionManager(MongoDatabaseFactory mongoDatabaseFactory){ + return new MongoTransactionManager(mongoDatabaseFactory); + } +} diff --git a/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/entity/Garbage.java b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/entity/Garbage.java new file mode 100644 index 0000000..8362a24 --- /dev/null +++ b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/entity/Garbage.java @@ -0,0 +1,34 @@ +package com.chushang.common.mongo.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.FieldType; +import org.springframework.data.mongodb.core.mapping.MongoId; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author by zhaowenyuan create 2022/7/18 12:27 + */ +@Data +@Document("garbage") +@AllArgsConstructor +@NoArgsConstructor +public class Garbage implements Serializable { + private static final long serialVersionUID = 1L; + + @MongoId(FieldType.OBJECT_ID) + private String id; + + private String type; + + private E data; + + @Indexed + private LocalDateTime collectedTime; + +} diff --git a/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/listener/MongoEventListener.java b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/listener/MongoEventListener.java new file mode 100644 index 0000000..e82a85d --- /dev/null +++ b/chushang-common/chushang-common-mongo/src/main/java/com/chushang/common/mongo/listener/MongoEventListener.java @@ -0,0 +1,50 @@ +package com.chushang.common.mongo.listener; + +import com.chushang.common.mongo.annotation.MongoDel; +import com.chushang.common.mongo.entity.Garbage; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author by zhaowenyuan create 2022/7/18 14:06 + * + * // 当使用此方法删除时, 需注 + * 1. 不能使用 deleteAll 因为其doRemove 时未指定实体类型, 而回调事件时需要实体类型不能为空 + * 2. 直接使用 mongoTemplate.remove 方法删除数据时,必须指定 entityClass + */ +public class MongoEventListener extends AbstractMongoEventListener { + + private final MongoTemplate mongoTemplate; + + public MongoEventListener(MongoTemplate mongoTemplate){ + this.mongoTemplate = mongoTemplate; + } + + @Override + public void onBeforeDelete(BeforeDeleteEvent event) { + if (null == event.getType() || CollectionUtils.isEmpty(event.getDocument())){ + return; + } + Class eventType = event.getType(); + MongoDel mongoDel = eventType.getAnnotation(MongoDel.class); + if (null == mongoDel || !mongoDel.logic()) return; + List es = mongoTemplate.find(new BasicQuery(event.getDocument()), eventType); + if (!CollectionUtils.isEmpty(es)){ + // 删除之前将数据迁移搭到 garbage 中 + String type = event.getCollectionName(); + LocalDateTime now = LocalDateTime.now(); + List garbageList = + es.stream().map(o -> new Garbage(null, type, o, now)).collect(Collectors.toList()); + mongoTemplate.insertAll(garbageList); + } + + + } +} diff --git a/chushang-common/chushang-common-mongo/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-mongo/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..60f30cc --- /dev/null +++ b/chushang-common/chushang-common-mongo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.common.mongo.listener.MongoEventListener,\ + com.chushang.common.mongo.config.MongoTransaction diff --git a/chushang-common/chushang-common-mybatis/pom.xml b/chushang-common/chushang-common-mybatis/pom.xml new file mode 100644 index 0000000..e4f691c --- /dev/null +++ b/chushang-common/chushang-common-mybatis/pom.xml @@ -0,0 +1,47 @@ + + + + chushang-common + com.chushang + 1.0.0 + + jar + 4.0.0 + + chushang-common-mybatis + + + 17 + 17 + + + + + com.baomidou + mybatis-plus-boot-starter + + + cn.hutool + hutool-all + + + mysql + mysql-connector-java + + + javax.servlet + javax.servlet-api + + + org.springframework.boot + spring-boot-starter-web + + + com.github.jsqlparser + jsqlparser + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/core/config/GlobalConfig.java b/chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/core/config/GlobalConfig.java new file mode 100644 index 0000000..6a2f347 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/core/config/GlobalConfig.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2011-2022, baomidou (jobob@qq.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.mybatisplus.core.config; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.injector.ISqlInjector; +import com.baomidou.mybatisplus.core.mapper.Mapper; +import lombok.Data; +import lombok.experimental.Accessors; +import org.apache.ibatis.session.SqlSessionFactory; + +import java.io.Serializable; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * Mybatis 全局缓存 + * + * @author Caratacus + * @since 2016-12-06 + */ +@Data +@Accessors(chain = true) +@SuppressWarnings("serial") +public class GlobalConfig implements Serializable { + /** + * 是否开启 LOGO + */ + private boolean banner = false; + /** + * 是否初始化 SqlRunner + */ + private boolean enableSqlRunner = false; + + private boolean refreshMapper = false; + /** + * 数据库相关配置 + */ + private DbConfig dbConfig; + /** + * SQL注入器 + */ + private ISqlInjector sqlInjector = new DefaultSqlInjector(); + /** + * Mapper父类 + */ + private Class superMapperClass = Mapper.class; + /** + * 仅用于缓存 SqlSessionFactory(外部勿进行set,set了也没用) + */ + private SqlSessionFactory sqlSessionFactory; + /** + * 缓存已注入CRUD的Mapper信息 + */ + private Set mapperRegistryCache = new ConcurrentSkipListSet<>(); + /** + * 元对象字段填充控制器 + */ + private MetaObjectHandler metaObjectHandler; + /** + * 主键生成器 + */ + private IdentifierGenerator identifierGenerator; + + @Data + public static class DbConfig { + /** + * 主键类型 + */ + private IdType idType = IdType.ASSIGN_ID; + /** + * 表名前缀 + */ + private String tablePrefix; + /** + * schema + * + * @since 3.1.1 + */ + private String schema; + /** + * db字段 format + *

+ * 例: `%s` + *

+ * 对主键无效 + * + * @since 3.1.1 + */ + private String columnFormat; + /** + * entity 的字段(property)的 format,只有在 column as property 这种情况下生效 + *

+ * 例: `%s` + *

+ * 对主键无效 + * + * @since 3.3.0 + */ + private String propertyFormat; + /** + * 实验性功能,占位符替换,等同于 {@link com.baomidou.mybatisplus.extension.plugins.inner.ReplacePlaceholderInnerInterceptor}, + * 只是这个属于启动时替换,用得地方多会启动慢一点点,不适用于其他的 {@link org.apache.ibatis.scripting.LanguageDriver} + * + * @since 3.4.2 + */ + private boolean replacePlaceholder; + /** + * 转义符 + *

+ * 配合 {@link #replacePlaceholder} 使用时有效 + *

+ * 例: " 或 ' 或 ` + * + * @since 3.4.2 + */ + private String escapeSymbol; + /** + * 表名是否使用驼峰转下划线命名,只对表名生效 + */ + private boolean tableUnderline = true; + /** + * 大写命名,对表名和字段名均生效 + */ + private boolean capitalMode = false; + /** + * 表主键生成器 + */ + private List keyGenerators; + /** + * 逻辑删除全局属性名 + */ + private String logicDeleteField; + /** + * 逻辑删除全局值(默认 1、表示已删除) + */ + private String logicDeleteValue = "1"; + /** + * 逻辑未删除全局值(默认 0、表示未删除) + */ + private String logicNotDeleteValue = "0"; + /** + * 字段验证策略之 insert + * + * @since 3.1.2 + */ + private FieldStrategy insertStrategy = FieldStrategy.NOT_NULL; + /** + * 字段验证策略之 update + * + * @since 3.1.2 + */ + private FieldStrategy updateStrategy = FieldStrategy.NOT_NULL; + + /** + * 字段验证策略之 select + * + * @since 3.1.2 + * @deprecated 3.4.4 + */ + @Deprecated + private FieldStrategy selectStrategy; + + /** + * 字段验证策略之 where + * 替代selectStrategy,保持与{@link TableField#whereStrategy()}一致 + * + * @since 3.4.4 + */ + private FieldStrategy whereStrategy = FieldStrategy.NOT_NULL; + + /** + * 重写whereStrategy的get方法,适配低版本: + * - 如果用户自定义了selectStrategy则用用户自定义的, + * - 后续版本移除selectStrategy后,直接删除该方法即可。 + * + * @return 字段作为查询条件时的验证策略 + * @since 3.4.4 + */ + public FieldStrategy getWhereStrategy() { + return selectStrategy == null ? whereStrategy : selectStrategy; + } + } +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/extension/plugins/inner/PaginationInnerInterceptor.java b/chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/extension/plugins/inner/PaginationInnerInterceptor.java new file mode 100644 index 0000000..d65cb95 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/baomidou/mybatisplus/extension/plugins/inner/PaginationInnerInterceptor.java @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2011-2022, baomidou (jobob@qq.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.mybatisplus.extension.plugins.inner; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.core.toolkit.*; +import com.baomidou.mybatisplus.extension.plugins.pagination.DialectFactory; +import com.baomidou.mybatisplus.extension.plugins.pagination.DialectModel; +import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect; +import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils; +import com.baomidou.mybatisplus.extension.toolkit.PropertyMapper; +import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils; +import lombok.Data; +import lombok.NoArgsConstructor; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.*; +import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 分页拦截器 + *

+ * 默认对 left join 进行优化,虽然能优化count,但是加上分页的话如果1对多本身结果条数就是不正确的 + * + * @author hubin + * @since 3.4.0 + * + * 修改无法使用原生sql 的 warn 打印 + */ +@Data +@NoArgsConstructor +@SuppressWarnings({"rawtypes"}) +public class PaginationInnerInterceptor implements InnerInterceptor { + /** + * 获取jsqlparser中count的SelectItem + */ + protected static final List COUNT_SELECT_ITEM = Collections.singletonList( + new SelectExpressionItem(new Column().withColumnName("COUNT(*)")).withAlias(new Alias("total")) + ); + protected static final Map countMsCache = new ConcurrentHashMap<>(); + protected final Log logger = LogFactory.getLog(this.getClass()); + + + /** + * 溢出总页数后是否进行处理 + */ + protected boolean overflow; + /** + * 单页分页条数限制 + */ + protected Long maxLimit; + /** + * 数据库类型 + *

+ * 查看 {@link #findIDialect(Executor)} 逻辑 + */ + private DbType dbType; + /** + * 方言实现类 + *

+ * 查看 {@link #findIDialect(Executor)} 逻辑 + */ + private IDialect dialect; + /** + * 生成 countSql 优化掉 join + * 现在只支持 left join + * + * @since 3.4.2 + */ + protected boolean optimizeJoin = true; + + public PaginationInnerInterceptor(DbType dbType) { + this.dbType = dbType; + } + + public PaginationInnerInterceptor(IDialect dialect) { + this.dialect = dialect; + } + + /** + * 这里进行count,如果count为0这返回false(就是不再执行sql了) + */ + @Override + public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + IPage page = ParameterUtils.findPage(parameter).orElse(null); + if (page == null || page.getSize() < 0 || !page.searchCount()) { + return true; + } + + BoundSql countSql; + MappedStatement countMs = buildCountMappedStatement(ms, page.countId()); + if (countMs != null) { + countSql = countMs.getBoundSql(parameter); + } else { + countMs = buildAutoCountMappedStatement(ms); + String countSqlStr = autoCountSql(page, boundSql.getSql()); + PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql); + countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter); + PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters()); + } + + CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql); + List result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql); + long total = 0; + if (CollectionUtils.isNotEmpty(result)) { + // 个别数据库 count 没数据不会返回 0 + Object o = result.get(0); + if (o != null) { + total = Long.parseLong(o.toString()); + } + } + page.setTotal(total); + return continuePage(page); + } + + @Override + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + IPage page = ParameterUtils.findPage(parameter).orElse(null); + if (null == page) { + return; + } + + // 处理 orderBy 拼接 + boolean addOrdered = false; + String buildSql = boundSql.getSql(); + List orders = page.orders(); + if (CollectionUtils.isNotEmpty(orders)) { + addOrdered = true; + buildSql = this.concatOrderBy(buildSql, orders); + } + + // size 小于 0 且不限制返回值则不构造分页sql + Long _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit; + if (page.getSize() < 0 && null == _limit) { + if (addOrdered) { + PluginUtils.mpBoundSql(boundSql).sql(buildSql); + } + return; + } + + handlerLimit(page, _limit); + IDialect dialect = findIDialect(executor); + + final Configuration configuration = ms.getConfiguration(); + DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize()); + PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql); + + List mappings = mpBoundSql.parameterMappings(); + Map additionalParameter = mpBoundSql.additionalParameters(); + model.consumers(mappings, configuration, additionalParameter); + mpBoundSql.sql(model.getDialectSql()); + mpBoundSql.parameterMappings(mappings); + } + + /** + * 获取分页方言类的逻辑 + * + * @param executor Executor + * @return 分页方言类 + */ + protected IDialect findIDialect(Executor executor) { + if (dialect != null) { + return dialect; + } + if (dbType != null) { + dialect = DialectFactory.getDialect(dbType); + return dialect; + } + return DialectFactory.getDialect(JdbcUtils.getDbType(executor)); + } + + /** + * 获取指定的 id 的 MappedStatement + * + * @param ms MappedStatement + * @param countId id + * @return MappedStatement + */ + protected MappedStatement buildCountMappedStatement(MappedStatement ms, String countId) { + if (StringUtils.isNotBlank(countId)) { + final String id = ms.getId(); + if (!countId.contains(StringPool.DOT)) { + countId = id.substring(0, id.lastIndexOf(StringPool.DOT) + 1) + countId; + } + final Configuration configuration = ms.getConfiguration(); + try { + return CollectionUtils.computeIfAbsent(countMsCache, countId, key -> configuration.getMappedStatement(key, false)); + } catch (Exception e) { + logger.warn(String.format("can not find this countId: [\"%s\"]", countId)); + } + } + return null; + } + + /** + * 构建 mp 自用自动的 MappedStatement + * + * @param ms MappedStatement + * @return MappedStatement + */ + protected MappedStatement buildAutoCountMappedStatement(MappedStatement ms) { + final String countId = ms.getId() + "_mpCount"; + final Configuration configuration = ms.getConfiguration(); + return CollectionUtils.computeIfAbsent(countMsCache, countId, key -> { + MappedStatement.Builder builder = new MappedStatement.Builder(configuration, key, ms.getSqlSource(), ms.getSqlCommandType()); + builder.resource(ms.getResource()); + builder.fetchSize(ms.getFetchSize()); + builder.statementType(ms.getStatementType()); + builder.timeout(ms.getTimeout()); + builder.parameterMap(ms.getParameterMap()); + builder.resultMaps(Collections.singletonList(new ResultMap.Builder(configuration, Constants.MYBATIS_PLUS, Long.class, Collections.emptyList()).build())); + builder.resultSetType(ms.getResultSetType()); + builder.cache(ms.getCache()); + builder.flushCacheRequired(ms.isFlushCacheRequired()); + builder.useCache(ms.isUseCache()); + return builder.build(); + }); + } + + /** + * 获取自动优化的 countSql + * + * @param page 参数 + * @param sql sql + * @return countSql + */ + protected String autoCountSql(IPage page, String sql) { + if (!page.optimizeCountSql()) { + return lowLevelCountSql(sql); + } + try { + Select select = (Select) CCJSqlParserUtil.parse(sql); + SelectBody selectBody = select.getSelectBody(); + // https://github.com/baomidou/mybatis-plus/issues/3920 分页增加union语法支持 + if (selectBody instanceof SetOperationList) { + return lowLevelCountSql(sql); + } + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + Distinct distinct = plainSelect.getDistinct(); + GroupByElement groupBy = plainSelect.getGroupBy(); + List orderBy = plainSelect.getOrderByElements(); + + if (CollectionUtils.isNotEmpty(orderBy)) { + boolean canClean = true; + if (groupBy != null) { + // 包含groupBy 不去除orderBy + canClean = false; + } + if (canClean) { + for (OrderByElement order : orderBy) { + // order by 里带参数,不去除order by + Expression expression = order.getExpression(); + if (!(expression instanceof Column) && expression.toString().contains(StringPool.QUESTION_MARK)) { + canClean = false; + break; + } + } + } + if (canClean) { + plainSelect.setOrderByElements(null); + } + } + //#95 Github, selectItems contains #{} ${}, which will be translated to ?, and it may be in a function: power(#{myInt},2) + for (SelectItem item : plainSelect.getSelectItems()) { + if (item.toString().contains(StringPool.QUESTION_MARK)) { + return lowLevelCountSql(select.toString()); + } + } + // 包含 distinct、groupBy不优化 + if (distinct != null || null != groupBy) { + return lowLevelCountSql(select.toString()); + } + // 包含 join 连表,进行判断是否移除 join 连表 + if (optimizeJoin && page.optimizeJoinOfCountSql()) { + List joins = plainSelect.getJoins(); + if (CollectionUtils.isNotEmpty(joins)) { + boolean canRemoveJoin = true; + String whereS = Optional.ofNullable(plainSelect.getWhere()).map(Expression::toString).orElse(StringPool.EMPTY); + // 不区分大小写 + whereS = whereS.toLowerCase(); + for (Join join : joins) { + if (!join.isLeft()) { + canRemoveJoin = false; + break; + } + FromItem rightItem = join.getRightItem(); + String str = ""; + if (rightItem instanceof Table) { + Table table = (Table) rightItem; + str = Optional.ofNullable(table.getAlias()).map(Alias::getName).orElse(table.getName()) + StringPool.DOT; + } else if (rightItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) rightItem; + /* 如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join */ + if (subSelect.toString().contains(StringPool.QUESTION_MARK)) { + canRemoveJoin = false; + break; + } + str = subSelect.getAlias().getName() + StringPool.DOT; + } + // 不区分大小写 + str = str.toLowerCase(); + String onExpressionS = join.getOnExpression().toString(); + /* 如果 join 里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join */ + if (onExpressionS.contains(StringPool.QUESTION_MARK) || whereS.contains(str)) { + canRemoveJoin = false; + break; + } + } + if (canRemoveJoin) { + plainSelect.setJoins(null); + } + } + } + // 优化 SQL + plainSelect.setSelectItems(COUNT_SELECT_ITEM); + return select.toString(); + } catch (JSQLParserException e) { + // 无法优化使用原 SQL + logger.warn("无法优化使用原生sql"); + } catch (Exception e) { + logger.warn("optimize this sql to a count sql has error, sql:\"" + sql + "\", exception:\n" + e); + } + return lowLevelCountSql(sql); + } + + /** + * 无法进行count优化时,降级使用此方法 + * + * @param originalSql 原始sql + * @return countSql + */ + protected String lowLevelCountSql(String originalSql) { + return SqlParserUtils.getOriginalCountSql(originalSql); + } + + /** + * 查询SQL拼接Order By + * + * @param originalSql 需要拼接的SQL + * @return ignore + */ + public String concatOrderBy(String originalSql, List orderList) { + try { + Select select = (Select) CCJSqlParserUtil.parse(originalSql); + SelectBody selectBody = select.getSelectBody(); + if (selectBody instanceof PlainSelect) { + PlainSelect plainSelect = (PlainSelect) selectBody; + List orderByElements = plainSelect.getOrderByElements(); + List orderByElementsReturn = addOrderByElements(orderList, orderByElements); + plainSelect.setOrderByElements(orderByElementsReturn); + return select.toString(); + } else if (selectBody instanceof SetOperationList) { + SetOperationList setOperationList = (SetOperationList) selectBody; + List orderByElements = setOperationList.getOrderByElements(); + List orderByElementsReturn = addOrderByElements(orderList, orderByElements); + setOperationList.setOrderByElements(orderByElementsReturn); + return select.toString(); + } else if (selectBody instanceof WithItem) { + // todo: don't known how to resole + return originalSql; + } else { + return originalSql; + } + } catch (JSQLParserException e) { + logger.warn("failed to concat orderBy from IPage, exception:\n" + e.getCause()); + } catch (Exception e) { + logger.warn("failed to concat orderBy from IPage, exception:\n" + e); + } + return originalSql; + } + + protected List addOrderByElements(List orderList, List orderByElements) { + List additionalOrderBy = orderList.stream() + .filter(item -> StringUtils.isNotBlank(item.getColumn())) + .map(item -> { + OrderByElement element = new OrderByElement(); + element.setExpression(new Column(item.getColumn())); + element.setAsc(item.isAsc()); + element.setAscDescPresent(true); + return element; + }).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(orderByElements)) { + return additionalOrderBy; + } + // github pull/3550 优化排序,比如:默认 order by id 前端传了name排序,设置为 order by name,id + additionalOrderBy.addAll(orderByElements); + return additionalOrderBy; + } + + /** + * count 查询之后,是否继续执行分页 + * + * @param page 分页对象 + * @return 是否 + */ + protected boolean continuePage(IPage page) { + if (page.getTotal() <= 0) { + return false; + } + if (page.getCurrent() > page.getPages()) { + if (overflow) { + //溢出总页数处理 + handlerOverflow(page); + } else { + // 超过最大范围,未设置溢出逻辑中断 list 执行 + return false; + } + } + return true; + } + + /** + * 处理超出分页条数限制,默认归为限制数 + * + * @param page IPage + */ + protected void handlerLimit(IPage page, Long limit) { + final long size = page.getSize(); + if (limit != null && limit > 0 && (size > limit || size < 0)) { + page.setSize(limit); + } + } + + /** + * 处理页数溢出,默认设置为第一页 + * + * @param page IPage + */ + protected void handlerOverflow(IPage page) { + page.setCurrent(1); + } + + @Override + public void setProperties(Properties properties) { + PropertyMapper.newInstance(properties) + .whenNotBlank("overflow", Boolean::parseBoolean, this::setOverflow) + .whenNotBlank("dbType", DbType::getDbType, this::setDbType) + .whenNotBlank("dialect", ClassUtils::newInstance, this::setDialect) + .whenNotBlank("maxLimit", Long::parseLong, this::setMaxLimit) + .whenNotBlank("optimizeJoin", Boolean::parseBoolean, this::setOptimizeJoin); + } +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/MybatisAutoConfiguration.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/MybatisAutoConfiguration.java new file mode 100644 index 0000000..2396263 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/MybatisAutoConfiguration.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.common.mybatis; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.chushang.common.mybatis.config.MybatisPlusMapperRefresh; +import com.chushang.common.mybatis.config.MybatisPlusMetaObjectHandler; +import com.chushang.common.mybatis.resolver.SqlFilterArgumentResolver; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * @author lengleng + * @date 2020-03-14 + *

+ * mybatis plus 统一配置 + */ +@Configuration(proxyBeanMethods = false) +@MapperScan({"com.chushang.**.mapper"}) +public class MybatisAutoConfiguration implements WebMvcConfigurer { + + @Autowired + MybatisPlusProperties mybatisPlusProperties; + + /** + * SQL 过滤器避免SQL 注入 + * @param argumentResolvers + */ + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new SqlFilterArgumentResolver()); + } + + /** + * + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件, 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + // 乐观锁 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + return interceptor; + } + + /** + * 审计字段自动填充 + * @return {@link MetaObjectHandler} + */ + @Bean + public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() { + return new MybatisPlusMetaObjectHandler(); + } + + @Bean + public MybatisPlusMapperRefresh mybatisPlusMapperRefresh(ApplicationContext applicationContext, SqlSessionFactory sqlSessionFactory){ + Set mapperLocations = new LinkedHashSet<>(); + for (String xx : mybatisPlusProperties.getMapperLocations()) { + try { + mapperLocations.addAll(Arrays.asList(applicationContext.getResources(xx))); + } catch (Exception ignored) { + } + } + return new MybatisPlusMapperRefresh(mapperLocations.toArray(new Resource[mapperLocations.size()]), + sqlSessionFactory, + 10, + 5); + } + +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/base/BaseEntity.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/base/BaseEntity.java new file mode 100644 index 0000000..7b27978 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/base/BaseEntity.java @@ -0,0 +1,78 @@ +package com.chushang.common.mybatis.base; + +import cn.hutool.core.date.DatePattern; +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 抽象实体 + */ +@Data +public class BaseEntity implements Serializable { + /** + * 创建时间 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @TableField(value = "create_time",fill = FieldFill.INSERT, updateStrategy = FieldStrategy.NEVER) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + protected LocalDateTime createTime; + /** + * 更新时间 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE, updateStrategy = FieldStrategy.NOT_NULL) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + protected LocalDateTime updateTime; + /** + * 删除状态 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @TableLogic(value = "false",delval = "true") + protected Boolean delState; + /** + * 版本号 + */ + @Version + @TableField( + value = "version", + update = "%s+1", + fill = FieldFill.INSERT + ) + protected Integer version; + + @TableField(exist = false) + private transient Map sqlParam; + + public Map getSqlParam() + { + if (sqlParam == null) + { + sqlParam = new HashMap<>(); + } + return sqlParam; + } + + @Serial + private static final long serialVersionUID = 1L; + +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMapperRefresh.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMapperRefresh.java new file mode 100644 index 0000000..5f957ac --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMapperRefresh.java @@ -0,0 +1,274 @@ +package com.chushang.common.mybatis.config; + +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils; +import com.baomidou.mybatisplus.core.toolkit.SystemClock; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.binding.MapperRegistry; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.builder.xml.XMLMapperEntityResolver; +import org.apache.ibatis.executor.ErrorContext; +import org.apache.ibatis.executor.keygen.SelectKeyGenerator; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.parsing.XPathParser; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.util.ResourceUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; + +import static java.lang.Thread.sleep; + +/** + *

Mybatis 映射文件热加载(发生变动后自动重新加载).

+ *

方便开发时使用,不用每次修改xml文件后都要去重启应用.

+ */ +@Slf4j +public class MybatisPlusMapperRefresh implements Runnable { + /** + * 记录jar包存在的mapper + */ + private static final Map> jarMapper = new HashMap<>(); + private final SqlSessionFactory sqlSessionFactory; + private final Resource[] mapperLocations; + private volatile Long beforeTime = 0L; + private Configuration configuration; + /** + * xml文件目录 + */ + private Set fileSet; + /** + * 延迟加载时间 + */ + private int delaySeconds = 10; + /** + * 刷新间隔时间 + */ + private int sleepSeconds = 20; + + public MybatisPlusMapperRefresh(Resource[] mapperLocations, SqlSessionFactory sqlSessionFactory, int delaySeconds, + int sleepSeconds) { + this.mapperLocations = mapperLocations.clone(); + this.sqlSessionFactory = sqlSessionFactory; + this.delaySeconds = delaySeconds; + this.sleepSeconds = sleepSeconds; + this.configuration = sqlSessionFactory.getConfiguration(); + this.run(); + } + + public MybatisPlusMapperRefresh(Resource[] mapperLocations, SqlSessionFactory sqlSessionFactory) { + this.mapperLocations = mapperLocations.clone(); + this.sqlSessionFactory = sqlSessionFactory; + this.configuration = sqlSessionFactory.getConfiguration(); + this.run(); + } + + @Override + public void run() { + final GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration); + /* + */ + if (globalConfig.isRefreshMapper()) { + beforeTime = SystemClock.now(); + final MybatisPlusMapperRefresh runnable = this; + new Thread(() -> { + if (fileSet == null) { + fileSet = new HashSet<>(); + if (mapperLocations != null) { + for (Resource mapperLocation : mapperLocations) { + try { + if (ResourceUtils.isJarURL(mapperLocation.getURL())) { + String key = new UrlResource( + ResourceUtils.extractJarFileURL(mapperLocation.getURL())).getFile() + .getPath(); + fileSet.add(key); + if (jarMapper.get(key) != null) { + jarMapper.get(key).add(mapperLocation); + } else { + List resourcesList = new ArrayList<>(); + resourcesList.add(mapperLocation); + jarMapper.put(key, resourcesList); + } + } else { + fileSet.add(mapperLocation.getFile().getPath()); + } + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } + } + } + try { + sleep(delaySeconds * 1000L); + } catch (InterruptedException interruptedException) { + interruptedException.printStackTrace(); + } + do { + try { + for (String filePath : fileSet) { + File file = new File(filePath); + if (file.isFile() && file.lastModified() > beforeTime) { + // 记录上次重新加载时间防止重复加载已经重载的文件 + beforeTime = file.lastModified(); + List removeList = jarMapper.get(filePath); + if (removeList != null && !removeList.isEmpty()) { + for (Resource resource : removeList) { + runnable.refresh(resource); + } + } else { + runnable.refresh(new FileSystemResource(file)); + } + } + } + } catch (Exception exception) { + exception.printStackTrace(); + } + try { + sleep(sleepSeconds * 1000L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } while (true); + }, "mybatis-plus MapperRefresh").start(); + } + } + + /** + * 刷新mapper + * + * @throws Exception + */ + @SuppressWarnings("rawtypes") + private void refresh(Resource resource) + throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + this.configuration = sqlSessionFactory.getConfiguration(); + boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class; + try { + Field loadedResourcesField = isSupper + ? configuration.getClass().getSuperclass().getDeclaredField("loadedResources") + : configuration.getClass().getDeclaredField("loadedResources"); + loadedResourcesField.setAccessible(true); + Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration)); + XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(), + new XMLMapperEntityResolver()); + XNode context = xPathParser.evalNode("/mapper"); + String namespace = context.getStringAttribute("namespace"); + Field field = MapperRegistry.class.getDeclaredField("knownMappers"); + field.setAccessible(true); + Map mapConfig = (Map) field.get(configuration.getMapperRegistry()); + + mapConfig.remove(Resources.classForName(namespace)); + loadedResourcesSet.remove(resource.toString()); + configuration.getCacheNames().remove(namespace); + + cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace); + cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace); + cleanKeyGenerators(context.evalNodes("insert|update|select|delete"), namespace); + cleanSqlElement(context.evalNodes("/mapper/sql"), namespace); + XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(), + sqlSessionFactory.getConfiguration(), resource.toString(), + sqlSessionFactory.getConfiguration().getSqlFragments()); + xmlMapperBuilder.parse(); + log.debug("refresh: '" + resource + "', success!"); + } catch (IOException e) { + log.error("Refresh IOException :" + e.getMessage()); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 清理parameterMap + * + * @param list + * @param namespace + */ + private void cleanParameterMap(List list, String namespace) { + for (XNode parameterMapNode : list) { + String id = parameterMapNode.getStringAttribute("id"); + configuration.getParameterMaps().remove(String.format("%s.%s", namespace, id)); + } + } + + /** + * 清理resultMap + * + * @param list + * @param namespace + */ + private void cleanResultMap(List list, String namespace) { + for (XNode resultMapNode : list) { + String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); + configuration.getResultMapNames().remove(id); + configuration.getResultMapNames().remove(namespace + "." + id); + clearResultMap(resultMapNode, namespace); + } + } + + private void clearResultMap(XNode xNode, String namespace) { + for (XNode resultChild : xNode.getChildren()) { + if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName()) + || "case".equals(resultChild.getName())) { + if (resultChild.getStringAttribute("select") == null) { + configuration.getResultMapNames() + .remove(resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier())); + configuration.getResultMapNames().remove(namespace + "." + + resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier())); + if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) { + clearResultMap(resultChild, namespace); + } + } + } + } + } + + /** + * 清理selectKey + * + * @param list + * @param namespace + */ + private void cleanKeyGenerators(List list, String namespace) { + for (XNode context : list) { + String id = context.getStringAttribute("id"); + configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX); + configuration.getKeyGeneratorNames().remove(namespace + "." + id + SelectKeyGenerator.SELECT_KEY_SUFFIX); + + Collection mappedStatements = configuration.getMappedStatements(); + List objects = new ArrayList<>(); + for (Object object : mappedStatements) { + if (object instanceof MappedStatement) { + MappedStatement mappedStatement = (MappedStatement) object; + if (mappedStatement.getId().equals(namespace + "." + id)) { + objects.add(mappedStatement); + } + } + } + mappedStatements.removeAll(objects); + } + } + + /** + * 清理sql节点缓存 + * + * @param list + * @param namespace + */ + private void cleanSqlElement(List list, String namespace) { + for (XNode context : list) { + String id = context.getStringAttribute("id"); + configuration.getSqlFragments().remove(id); + configuration.getSqlFragments().remove(namespace + "." + id); + } + } +} + + diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMetaObjectHandler.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMetaObjectHandler.java new file mode 100644 index 0000000..8d413b4 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/config/MybatisPlusMetaObjectHandler.java @@ -0,0 +1,62 @@ +package com.chushang.common.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.util.ClassUtils; + +import java.nio.charset.Charset; +import java.time.LocalDateTime; + +/** + * MybatisPlus 自动填充配置 + * + * @author L.cm + */ +@Slf4j +public class MybatisPlusMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + if(log.isDebugEnabled()){ + log.debug("mybatis plus start insert fill ...."); + } + LocalDateTime now = LocalDateTime.now(); + + fillValIfNullByName("createTime", now, metaObject, false); + fillValIfNullByName("updateTime", now, metaObject, false); + } + + @Override + public void updateFill(MetaObject metaObject) { + log.debug("mybatis plus start update fill ...."); + fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true); + } + + /** + * 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置 + * @param fieldName 属性名 + * @param fieldVal 属性值 + * @param metaObject MetaObject + * @param isCover 是否覆盖原有值,避免更新操作手动入参 + */ + private static void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) { + // 1. 没有 get 方法 + if (!metaObject.hasSetter(fieldName)) { + return; + } + // 2. 如果用户有手动设置的值 + Object userSetValue = metaObject.getValue(fieldName); + String setValueStr = StrUtil.str(userSetValue, Charset.defaultCharset()); + if (StrUtil.isNotBlank(setValueStr) && !isCover) { + return; + } + // 3. field 类型相同时设置 + Class getterType = metaObject.getGetterType(fieldName); + if (ClassUtils.isAssignableValue(getterType, fieldVal)) { + metaObject.setValue(fieldName, fieldVal); + } + } + +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/enums/Operator.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/enums/Operator.java new file mode 100644 index 0000000..bc5c635 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/enums/Operator.java @@ -0,0 +1,55 @@ +package com.chushang.common.mybatis.enums; + +/** + * 比较符 + */ +public enum Operator { + + /** + * 等于 + */ + EQUAL("="), + + /** + * 小于 + */ + LESS_THAN("<"), + + /** + * 小于等于 + */ + LESS_THAN_OR_EQUAL("<="), + + /** + * 大于 + */ + GREATER_THAN(">"), + + /** + * 大于等于 + */ + GREATER_THAN_OR_EQUAL(">="), + /** + * limit 1 + */ + LIMIT_ONE("limit 1") + + ; + + /** + * 比较符 + */ + private final String character; + + Operator(String character) { + this.character = character; + } + + /** + * 获取比较符 + * @return {@link String} + */ + public String getCharacter() { + return character; + } +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/handler/MybatisExceptionHandler.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/handler/MybatisExceptionHandler.java new file mode 100644 index 0000000..fca483e --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/handler/MybatisExceptionHandler.java @@ -0,0 +1,20 @@ +package com.chushang.common.mybatis.handler; + +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.MyBatisSystemException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * by zhaowenyuan create 2022/6/8 18:26 + */ +@Slf4j +@RestControllerAdvice +public class MybatisExceptionHandler { + + @ExceptionHandler(MyBatisSystemException.class) + public void mybatisException(MyBatisSystemException e){ + throw new RuntimeException(e); + } + +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/resolver/SqlFilterArgumentResolver.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/resolver/SqlFilterArgumentResolver.java new file mode 100644 index 0000000..0687836 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/resolver/SqlFilterArgumentResolver.java @@ -0,0 +1,115 @@ +/* + * + * * Copyright (c) 2019-2020, 冷冷 (wangiegie@gmail.com). + * *

+ * * Licensed under the GNU Lesser General Public License 3.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * *

+ * * https://www.gnu.org/licenses/lgpl.html + * *

+ * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package com.chushang.common.mybatis.resolver; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author lengleng + * @date 2019-06-24 + *

+ * 解决Mybatis Plus Order By SQL注入问题 + */ +@Slf4j +public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver { + + private final static String[] KEYWORDS = { "master", "truncate", "insert", "select", "delete", "update", "declare", + "alter", "drop", "sleep" }; + + /** + * 判断Controller是否包含page 参数 + * @param parameter 参数 + * @return 是否过滤 + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(Page.class); + } + + /** + * @param parameter 入参集合 + * @param mavContainer model 和 view + * @param webRequest web相关 + * @param binderFactory 入参解析 + * @return 检查后新的page对象 + *

+ * page 只支持查询 GET .如需解析POST获取请求报文体处理 + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + + String[] ascs = request.getParameterValues("ascs"); + String[] descs = request.getParameterValues("descs"); + String current = request.getParameter("current"); + String size = request.getParameter("size"); + + Page page = new Page<>(); + if (StrUtil.isNotBlank(current)) { + page.setCurrent(Long.parseLong(current)); + } + + if (StrUtil.isNotBlank(size)) { + page.setSize(Long.parseLong(size)); + } + + List orderItemList = new ArrayList<>(); + Optional.ofNullable(ascs).ifPresent(s -> orderItemList.addAll( + Arrays.stream(s).filter(sqlInjectPredicate()).map(OrderItem::asc).collect(Collectors.toList()))); + Optional.ofNullable(descs).ifPresent(s -> orderItemList.addAll( + Arrays.stream(s).filter(sqlInjectPredicate()).map(OrderItem::desc).collect(Collectors.toList()))); + page.addOrder(orderItemList); + + return page; + } + + /** + * 判断用户输入里面有没有关键字 + * @return Predicate + */ + private Predicate sqlInjectPredicate() { + return sql -> { + for (String keyword : KEYWORDS) { + if (StrUtil.containsIgnoreCase(sql, keyword)) { + return false; + } + } + return true; + }; + } + +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageClass.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageClass.java new file mode 100644 index 0000000..331c617 --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageClass.java @@ -0,0 +1,34 @@ +package com.chushang.common.mybatis.utils; + +import java.util.LinkedList; +import java.util.List; + +public class PageClass { + /** + * @param page page + * @param pageSize pageSize + * @param resultList 排序好的集合 + */ + public List page(Integer page, Integer pageSize, List resultList){ + if (null == page || null == pageSize){ + return resultList; + } + // 实际需要返回的结果集 + List resultDateList = new LinkedList<>(); + // 进行 分页 以及 + // 判断是否 大于 分页大小 -- 如果大于 -- 则返回的为 截取分页的数据 + if (resultList.size() > pageSize) { + int offset = pageSize * (page - 1); + List list; + if (resultList.size() <= offset + pageSize) { + list = resultList.subList(offset, resultList.size()); + } else { + list = resultList.subList(offset, offset + pageSize); + } + resultDateList.addAll(list); + } else { + resultDateList.addAll(resultList); + } + return resultDateList; + } +} diff --git a/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageUtils.java b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageUtils.java new file mode 100644 index 0000000..d6e30fa --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/java/com/chushang/common/mybatis/utils/PageUtils.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2016-2019 人人开源 All rights reserved. + * + * https://www.renren.io + * + * 版权所有,侵权必究! + */ +package com.chushang.common.mybatis.utils; + +import com.baomidou.mybatisplus.core.metadata.IPage; + +import java.io.Serializable; +import java.util.List; + +/** + * 分页工具类 + * + * @author Mark sunlightcs@gmail.com + */ +public class PageUtils implements Serializable { + private static final long serialVersionUID = 1L; + /** + * 总记录数 + */ + private int totalCount; + /** + * 每页记录数 + */ + private int pageSize; + /** + * 总页数 + */ + private int totalPage; + /** + * 当前页数 + */ + private int currPage; + /** + * 列表数据 + */ + private List list; + + /** + * 分页 + * @param list 列表数据 + * @param totalCount 总记录数 + * @param pageSize 每页记录数 + * @param currPage 当前页数 + */ + public PageUtils(List list, long totalCount, long pageSize, long currPage) { + this.list = list; + this.totalCount = (int)totalCount; + this.pageSize = (int)pageSize; + this.currPage = (int)currPage; + this.totalPage = (int)Math.ceil((double)totalCount/pageSize); + } + public PageUtils(List list, IPage page) { + this.list = list; + if (null != page){ + this.totalCount = (int)page.getTotal(); + this.pageSize = (int)page.getSize(); + this.currPage = (int)page.getCurrent(); + this.totalPage = (int)Math.ceil((double)totalCount/pageSize); + } + } + + /** + * 分页 + */ + public PageUtils(IPage page) { + this.list = page.getRecords(); + this.totalCount = (int)page.getTotal(); + this.pageSize = (int)page.getSize(); + this.currPage = (int)page.getCurrent(); + this.totalPage = (int)page.getPages(); + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public int getTotalPage() { + return totalPage; + } + + public void setTotalPage(int totalPage) { + this.totalPage = totalPage; + } + + public int getCurrPage() { + return currPage; + } + + public void setCurrPage(int currPage) { + this.currPage = currPage; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + +} diff --git a/chushang-common/chushang-common-mybatis/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-mybatis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..6bf21bb --- /dev/null +++ b/chushang-common/chushang-common-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.common.mybatis.MybatisAutoConfiguration, \ + com.chushang.common.mybatis.handler.MybatisExceptionHandler diff --git a/chushang-common/chushang-common-redis/pom.xml b/chushang-common/chushang-common-redis/pom.xml new file mode 100644 index 0000000..56e16f7 --- /dev/null +++ b/chushang-common/chushang-common-redis/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + chushang-common + com.chushang + 1.0.0 + + chushang-common-redis + + + com.chushang + chushang-common-core + + + org.redisson + redisson-spring-boot-starter + + + diff --git a/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/PlusSpringCacheManager.java b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/PlusSpringCacheManager.java new file mode 100644 index 0000000..dc8ec1a --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/PlusSpringCacheManager.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.chushang.redis.cache; + + +import com.chushang.redis.cache.utils.RedisUtils; +import org.redisson.api.RMap; +import org.redisson.api.RMapCache; +import org.redisson.spring.cache.CacheConfig; +import org.redisson.spring.cache.RedissonCache; +import org.springframework.boot.convert.DurationStyle; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.transaction.TransactionAwareCacheDecorator; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A {@link CacheManager} implementation + * backed by Redisson instance. + *

+ * 修改 RedissonSpringCacheManager 源码 + * 重写 cacheName 处理方法 支持多参数 + * + * @author Nikita Koksharov + * + */ +@SuppressWarnings("unchecked") +public class PlusSpringCacheManager implements CacheManager { + + private boolean dynamic = true; + + private boolean allowNullValues = true; + + private boolean transactionAware = true; + + Map configMap = new ConcurrentHashMap<>(); + ConcurrentMap instanceMap = new ConcurrentHashMap<>(); + + /** + * Creates CacheManager supplied by Redisson instance + */ + public PlusSpringCacheManager() { + } + + + /** + * Defines possibility of storing {@code null} values. + *

+ * Default is true + * + * @param allowNullValues stores if true + */ + public void setAllowNullValues(boolean allowNullValues) { + this.allowNullValues = allowNullValues; + } + + /** + * Defines if cache aware of Spring-managed transactions. + * If {@code true} put/evict operations are executed only for successful transaction in after-commit phase. + *

+ * Default is false + * + * @param transactionAware cache is transaction aware if true + */ + public void setTransactionAware(boolean transactionAware) { + this.transactionAware = transactionAware; + } + + /** + * Defines 'fixed' cache names. + * A new cache instance will not be created in dynamic for non-defined names. + *

+ * `null` parameter setups dynamic mode + * + * @param names of caches + */ + public void setCacheNames(Collection names) { + if (names != null) { + for (String name : names) { + getCache(name); + } + dynamic = false; + } else { + dynamic = true; + } + } + + /** + * Set cache config mapped by cache name + * + * @param config object + */ + public void setConfig(Map config) { + this.configMap = (Map) config; + } + + protected CacheConfig createDefaultConfig() { + return new CacheConfig(); + } + + @Override + public Cache getCache(String name) { + Cache cache = instanceMap.get(name); + if (cache != null) { + return cache; + } + if (!dynamic) { + return cache; + } + + CacheConfig config = configMap.get(name); + if (config == null) { + config = createDefaultConfig(); + configMap.put(name, config); + } + + // 重写 cacheName 支持多参数 + String[] array = StringUtils.delimitedListToStringArray(name, "#"); + name = array[0]; + if (array.length > 1) { + config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis()); + } + if (array.length > 2) { + config.setMaxIdleTime(DurationStyle.detectAndParse(array[2]).toMillis()); + } + if (array.length > 3) { + config.setMaxSize(Integer.parseInt(array[3])); + } + + if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) { + return createMap(name, config); + } + + return createMapCache(name, config); + } + + private Cache createMap(String name, CacheConfig config) { + RMap map = RedisUtils.getClient().getMap(name); + + Cache cache = new RedissonCache(map, allowNullValues); + if (transactionAware) { + cache = new TransactionAwareCacheDecorator(cache); + } + Cache oldCache = instanceMap.putIfAbsent(name, cache); + if (oldCache != null) { + cache = oldCache; + } + return cache; + } + + private Cache createMapCache(String name, CacheConfig config) { + RMapCache map = RedisUtils.getClient().getMapCache(name); + + Cache cache = new RedissonCache(map, config, allowNullValues); + if (transactionAware) { + cache = new TransactionAwareCacheDecorator(cache); + } + Cache oldCache = instanceMap.putIfAbsent(name, cache); + if (oldCache != null) { + cache = oldCache; + } else { + map.setMaxSize(config.getMaxSize()); + } + return cache; + } + + @Override + public Collection getCacheNames() { + return Collections.unmodifiableSet(configMap.keySet()); + } + + +} diff --git a/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/CacheUtils.java b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/CacheUtils.java new file mode 100644 index 0000000..f3bf708 --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/CacheUtils.java @@ -0,0 +1,75 @@ +package com.chushang.redis.cache.utils; + +import cn.hutool.extra.spring.SpringUtil; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.redisson.api.RMap; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import java.util.Set; + +/** + * 缓存操作工具类 {@link } + * + * @author Michelle.Chung + * @date 2022/8/13 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@SuppressWarnings(value = {"unchecked"}) +public class CacheUtils { + + private static final CacheManager CACHE_MANAGER = SpringUtil.getBean(CacheManager.class); + + /** + * 获取缓存组内所有的KEY + * + * @param cacheNames 缓存组名称 + */ + public static Set keys(String cacheNames) { + RMap rmap = (RMap) CACHE_MANAGER.getCache(cacheNames).getNativeCache(); + return rmap.keySet(); + } + + /** + * 获取缓存值 + * + * @param cacheNames 缓存组名称 + * @param key 缓存key + */ + public static T get(String cacheNames, Object key) { + Cache.ValueWrapper wrapper = CACHE_MANAGER.getCache(cacheNames).get(key); + return wrapper != null ? (T) wrapper.get() : null; + } + + /** + * 保存缓存值 + * + * @param cacheNames 缓存组名称 + * @param key 缓存key + * @param value 缓存值 + */ + public static void put(String cacheNames, Object key, Object value) { + CACHE_MANAGER.getCache(cacheNames).put(key, value); + } + + /** + * 删除缓存值 + * + * @param cacheNames 缓存组名称 + * @param key 缓存key + */ + public static void evict(String cacheNames, Object key) { + CACHE_MANAGER.getCache(cacheNames).evict(key); + } + + /** + * 清空缓存值 + * + * @param cacheNames 缓存组名称 + */ + public static void clear(String cacheNames) { + CACHE_MANAGER.getCache(cacheNames).clear(); + } + +} diff --git a/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/RedisUtils.java b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/RedisUtils.java new file mode 100644 index 0000000..f50117f --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/cache/utils/RedisUtils.java @@ -0,0 +1,463 @@ +package com.chushang.redis.cache.utils; + +import cn.hutool.extra.spring.SpringUtil; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.redisson.api.*; +import org.redisson.config.Config; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * redis 工具类 + * + * @author Lion Li + * @version 3.1.0 新增 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@SuppressWarnings(value = {"unchecked", "rawtypes"}) +public class RedisUtils { + + private static final RedissonClient CLIENT = SpringUtil.getBean(RedissonClient.class); + + public static NameMapper getNameMapper() { + Config config = CLIENT.getConfig(); + if (config.isClusterConfig()) { + return config.useClusterServers().getNameMapper(); + } + return config.useSingleServer().getNameMapper(); + } + + /** + * 限流 + * + * @param key 限流key + * @param rateType 限流类型 + * @param rate 速率 + * @param rateInterval 速率间隔 + * @return -1 表示失败 + */ + public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) { + RRateLimiter rateLimiter = CLIENT.getRateLimiter(key); + rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS); + if (rateLimiter.tryAcquire()) { + return rateLimiter.availablePermits(); + } else { + return -1L; + } + } + + /** + * 获取客户端实例 + */ + public static RedissonClient getClient() { + return CLIENT; + } + + /** + * 发布通道消息 + * + * @param channelKey 通道key + * @param msg 发送数据 + * @param consumer 自定义处理 + */ + public static void publish(String channelKey, T msg, Consumer consumer) { + RTopic topic = CLIENT.getTopic(channelKey); + topic.publish(msg); + consumer.accept(msg); + } + + public static void publish(String channelKey, T msg) { + RTopic topic = CLIENT.getTopic(channelKey); + topic.publish(msg); + } + + /** + * 订阅通道接收消息 + * + * @param channelKey 通道key + * @param clazz 消息类型 + * @param consumer 自定义处理 + */ + public static void subscribe(String channelKey, Class clazz, Consumer consumer) { + RTopic topic = CLIENT.getTopic(channelKey); + topic.addListener(clazz, (channel, msg) -> consumer.accept(msg)); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public static void setCacheObject(final String key, final T value) { + setCacheObject(key, value, false); + } + + /** + * 缓存基本的对象,保留当前对象 TTL 有效期 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param isSaveTtl 是否保留TTL有效期(例如: set之前ttl剩余90 set之后还是为90) + * @since Redis 6.X 以上使用 setAndKeepTTL 兼容 5.X 方案 + */ + public static void setCacheObject(final String key, final T value, final boolean isSaveTtl) { + RBucket bucket = CLIENT.getBucket(key); + if (isSaveTtl) { + try { + bucket.setAndKeepTTL(value); + } catch (Exception e) { + long timeToLive = bucket.remainTimeToLive(); + setCacheObject(key, value, Duration.ofMillis(timeToLive)); + } + } else { + bucket.set(value); + } + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param duration 时间 + */ + public static void setCacheObject(final String key, final T value, final Duration duration) { + RBatch batch = CLIENT.createBatch(); + RBucketAsync bucket = batch.getBucket(key); + bucket.setAsync(value); + bucket.expireAsync(duration); + batch.execute(); + } + + /** + * 注册对象监听器 + *

+ * key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置 + * + * @param key 缓存的键值 + * @param listener 监听器配置 + */ + public static void addObjectListener(final String key, final ObjectListener listener) { + RBucket result = CLIENT.getBucket(key); + result.addListener(listener); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public static boolean expire(final String key, final long timeout) { + return expire(key, Duration.ofSeconds(timeout)); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param duration 超时时间 + * @return true=设置成功;false=设置失败 + */ + public static boolean expire(final String key, final Duration duration) { + RBucket rBucket = CLIENT.getBucket(key); + return rBucket.expire(duration); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public static T getCacheObject(final String key) { + RBucket rBucket = CLIENT.getBucket(key); + return rBucket.get(); + } + + /** + * 获得key剩余存活时间 + * + * @param key 缓存键值 + * @return 剩余存活时间 + */ + public static long getTimeToLive(final String key) { + RBucket rBucket = CLIENT.getBucket(key); + return rBucket.remainTimeToLive(); + } + + /** + * 删除单个对象 + * + * @param key 缓存的键值 + */ + public static boolean deleteObject(final String key) { + return CLIENT.getBucket(key).delete(); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + */ + public static void deleteObject(final Collection collection) { + RBatch batch = CLIENT.createBatch(); + collection.forEach(t -> { + batch.getBucket(t.toString()).deleteAsync(); + }); + batch.execute(); + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public static boolean setCacheList(final String key, final List dataList) { + RList rList = CLIENT.getList(key); + return rList.addAll(dataList); + } + + /** + * 注册List监听器 + *

+ * key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置 + * + * @param key 缓存的键值 + * @param listener 监听器配置 + */ + public static void addListListener(final String key, final ObjectListener listener) { + RList rList = CLIENT.getList(key); + rList.addListener(listener); + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public static List getCacheList(final String key) { + RList rList = CLIENT.getList(key); + return rList.readAll(); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public static boolean setCacheSet(final String key, final Set dataSet) { + RSet rSet = CLIENT.getSet(key); + return rSet.addAll(dataSet); + } + + /** + * 注册Set监听器 + *

+ * key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置 + * + * @param key 缓存的键值 + * @param listener 监听器配置 + */ + public static void addSetListener(final String key, final ObjectListener listener) { + RSet rSet = CLIENT.getSet(key); + rSet.addListener(listener); + } + + /** + * 获得缓存的set + * + * @param key 缓存的key + * @return set对象 + */ + public static Set getCacheSet(final String key) { + RSet rSet = CLIENT.getSet(key); + return rSet.readAll(); + } + + /** + * 缓存Map + * + * @param key 缓存的键值 + * @param dataMap 缓存的数据 + */ + public static void setCacheMap(final String key, final Map dataMap) { + if (dataMap != null) { + RMap rMap = CLIENT.getMap(key); + rMap.putAll(dataMap); + } + } + + /** + * 注册Map监听器 + *

+ * key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置 + * + * @param key 缓存的键值 + * @param listener 监听器配置 + */ + public static void addMapListener(final String key, final ObjectListener listener) { + RMap rMap = CLIENT.getMap(key); + rMap.addListener(listener); + } + + /** + * 获得缓存的Map + * + * @param key 缓存的键值 + * @return map对象 + */ + public static Map getCacheMap(final String key) { + RMap rMap = CLIENT.getMap(key); + return rMap.getAll(rMap.keySet()); + } + + /** + * 获得缓存Map的key列表 + * + * @param key 缓存的键值 + * @return key列表 + */ + public static Set getCacheMapKeySet(final String key) { + RMap rMap = CLIENT.getMap(key); + return rMap.keySet(); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public static void setCacheMapValue(final String key, final String hKey, final T value) { + RMap rMap = CLIENT.getMap(key); + rMap.put(hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public static T getCacheMapValue(final String key, final String hKey) { + RMap rMap = CLIENT.getMap(key); + return rMap.get(hKey); + } + + /** + * 删除Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public static T delCacheMapValue(final String key, final String hKey) { + RMap rMap = CLIENT.getMap(key); + return rMap.remove(hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public static Map getMultiCacheMapValue(final String key, final Set hKeys) { + RMap rMap = CLIENT.getMap(key); + return rMap.getAll(hKeys); + } + + /** + * 设置原子值 + * + * @param key Redis键 + * @param value 值 + */ + public static void setAtomicValue(String key, long value) { + RAtomicLong atomic = CLIENT.getAtomicLong(key); + atomic.set(value); + } + + /** + * 获取原子值 + * + * @param key Redis键 + * @return 当前值 + */ + public static long getAtomicValue(String key) { + RAtomicLong atomic = CLIENT.getAtomicLong(key); + return atomic.get(); + } + + /** + * 递增原子值 + * + * @param key Redis键 + * @return 当前值 + */ + public static long incrAtomicValue(String key) { + RAtomicLong atomic = CLIENT.getAtomicLong(key); + return atomic.incrementAndGet(); + } + + /** + * 递减原子值 + * + * @param key Redis键 + * @return 当前值 + */ + public static long decrAtomicValue(String key) { + RAtomicLong atomic = CLIENT.getAtomicLong(key); + return atomic.decrementAndGet(); + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public static Collection keys(final String pattern) { + Stream stream = CLIENT.getKeys().getKeysStreamByPattern(getNameMapper().map(pattern)); + return stream.map(key -> getNameMapper().unmap(key)).collect(Collectors.toList()); + } + + /** + * 删除缓存的基本对象列表 + * + * @param pattern 字符串前缀 + */ + public static void deleteKeys(final String pattern) { + CLIENT.getKeys().deleteByPattern(getNameMapper().map(pattern)); + } + + /** + * 检查redis中是否存在key + * + * @param key 键 + */ + public static Boolean hasKey(String key) { + RKeys rKeys = CLIENT.getKeys(); + return rKeys.countExists(getNameMapper().map(key)) > 0; + } + +} diff --git a/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/RedisConfiguration.java b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/RedisConfiguration.java new file mode 100644 index 0000000..abc42f7 --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/RedisConfiguration.java @@ -0,0 +1,104 @@ +package com.chushang.redis.config; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.chushang.common.core.exception.utils.AssertUtil; +import com.chushang.common.core.util.StringUtils; +import com.chushang.redis.cache.PlusSpringCacheManager; +import com.chushang.redis.config.properties.RedissonProperties; +import com.chushang.redis.hanlder.KeyPrefixHandler; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.codec.JsonJacksonCodec; +import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; + +import java.util.List; +import java.util.stream.Collectors; + +/** + *

redis配置

+ * + * @author 单傲 + * @date 2023/1/17 16:27 + */ +@Slf4j +@AutoConfiguration +@EnableCaching +@RequiredArgsConstructor +@EnableConfigurationProperties(RedissonProperties.class) +public class RedisConfiguration extends CachingConfigurerSupport { + + private final RedissonProperties redissonProperties; + private final ObjectMapper objectMapper; + + + @Bean + public RedissonAutoConfigurationCustomizer redissonCustomizer() { + return config -> { + config.setThreads(redissonProperties.getThreads()) + .setNettyThreads(redissonProperties.getNettyThreads()) + .setCodec(new JsonJacksonCodec(objectMapper)) + ; + RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig(); + RedissonProperties.ClusterServersConfig clusterServerConfig = redissonProperties.getClusterServersConfig(); + RedissonProperties.SentinelServersConfig sentinelServerConfig = redissonProperties.getSentinelServersConfig(); + if (ObjectUtil.isNotNull(singleServerConfig)) { + String address = singleServerConfig.getAddress(); + AssertUtil.invalidate(StringUtils.isEmpty(address), "redisson 配置错误"); + // 使用单机模式 + config.useSingleServer() + //设置redis key前缀 + .setNameMapper(new KeyPrefixHandler(redissonProperties.getPrefix())) + .setTimeout(redissonProperties.getTimeout()) + .setPassword(redissonProperties.getPassword()) + .setUsername(redissonProperties.getUsername()) + .setDatabase(singleServerConfig.getDatabase()) + .setAddress(address) + ; + } + else + if (ObjectUtil.isNotNull(clusterServerConfig)) { + List nodes = clusterServerConfig.getNodes(); + AssertUtil.invalidate(CollectionUtil.isEmpty(nodes), "redisson cluster 配置错误"); + nodes = nodes.stream().map(s-> "redis://" + s).collect(Collectors.toList()); + config.useClusterServers() + //设置redis key前缀 + .setNameMapper(new KeyPrefixHandler(redissonProperties.getPrefix())) + .setTimeout(redissonProperties.getTimeout()) + .setPassword(redissonProperties.getPassword()) + .setUsername(redissonProperties.getUsername()) + .setNodeAddresses(nodes); + ; + } else if (ObjectUtil.isNotNull(sentinelServerConfig)){ + List nodes = clusterServerConfig.getNodes(); + AssertUtil.invalidate(CollectionUtil.isEmpty(nodes), "redisson sentinel 配置错误"); + nodes = nodes.stream().map(s-> "redis://" + s).collect(Collectors.toList()); + config.useSentinelServers() + .setNameMapper(new KeyPrefixHandler(redissonProperties.getPrefix())) + .setTimeout(redissonProperties.getTimeout()) + .setUsername(redissonProperties.getUsername()) + .setPassword(redissonProperties.getPassword()) + .setDatabase(sentinelServerConfig.getDatabase()) + .setMasterName(sentinelServerConfig.getMasterName()) + .setSentinelAddresses(nodes) + ; + } + log.info("------------- 初始化 redis 配置 -------------"); + }; + } + + /** + * 自定义缓存管理器 整合spring-cache + */ + @Bean + public CacheManager cacheManager() { + return new PlusSpringCacheManager(); + } +} diff --git a/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/properties/RedissonProperties.java b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/properties/RedissonProperties.java new file mode 100644 index 0000000..f557e9a --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/config/properties/RedissonProperties.java @@ -0,0 +1,100 @@ +package com.chushang.redis.config.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.redisson.config.ClusterServersConfig; +import org.redisson.config.ReadMode; +import org.redisson.config.SentinelServersConfig; +import org.redisson.config.SubscriptionMode; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * @auther: zhao + * @date: 2024/4/29 15:52 + */ +@Data +@RefreshScope +@Configuration +@ConfigurationProperties(prefix = "redis.config") +public class RedissonProperties { + + private String prefix; + /** + * 线程池数量,默认值 = 当前处理核数量 * 2 + */ + private int threads = 4; + + /** + * Netty线程池数量,默认值 = 当前处理核数量 * 2 + */ + private int nettyThreads = 8; + + private int timeout = 50000; + /** + * 密码 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 客户端名称 + */ + private String clientName; + /** + * 单机服务配置 + */ + private SingleServerConfig singleServerConfig; + /** + * 集群服务配置 + */ + private ClusterServersConfig clusterServersConfig; + /** + * 哨兵服务配置 + */ + private SentinelServersConfig sentinelServersConfig; + + @Data + @NoArgsConstructor + public static class SingleServerConfig { + /** + * 地址 + */ + private String address; + /** + * database + */ + private int database = 0; + } + @Data + @NoArgsConstructor + public static class ClusterServersConfig { + /** + * database + */ + private int database = 0; + /** + * 集群 + */ + private List nodes; + } + + @Data + @NoArgsConstructor + public static class SentinelServersConfig { + /** + * 主 + */ + private String masterName; + private int database = 0; + /** + * 地址 + */ + private List nodes; + } +} diff --git a/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/hanlder/KeyPrefixHandler.java b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/hanlder/KeyPrefixHandler.java new file mode 100644 index 0000000..b4aebb6 --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/java/com/chushang/redis/hanlder/KeyPrefixHandler.java @@ -0,0 +1,49 @@ +package com.chushang.redis.hanlder; + +import cn.hutool.core.util.StrUtil; +import org.redisson.api.NameMapper; + +/** + *

redis缓存key前缀处理

+ * + * @author 单傲 + * @date 2023/1/17 16:32 + */ +public class KeyPrefixHandler implements NameMapper { + + private final String keyPrefix; + + public KeyPrefixHandler(String keyPrefix) { + //前缀为空 则返回空前缀 + this.keyPrefix = StrUtil.isBlank(keyPrefix) ? "" : keyPrefix + ":"; + } + + /** + * 增加前缀 + */ + @Override + public String map(String name) { + if (StrUtil.isBlank(name)) { + return null; + } + if (StrUtil.isNotBlank(keyPrefix) && !name.startsWith(keyPrefix)) { + return keyPrefix + name; + } + return name; + } + + /** + * 去除前缀 + */ + @Override + public String unmap(String name) { + if (StrUtil.isBlank(name)) { + return null; + } + if (StrUtil.isNotBlank(keyPrefix) && name.startsWith(keyPrefix)) { + return name.substring(keyPrefix.length()); + } + return name; + } + +} diff --git a/chushang-common/chushang-common-redis/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..464ea43 --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.redis.config.RedisConfiguration diff --git a/chushang-common/chushang-common-redis/src/main/resources/redisson-sentnel.yml b/chushang-common/chushang-common-redis/src/main/resources/redisson-sentnel.yml new file mode 100644 index 0000000..93befb5 --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/resources/redisson-sentnel.yml @@ -0,0 +1,28 @@ +spring: + redis: + redisson: + config: | + sentinelServersConfig: + idleConnectionTimeout: 10000 + connectTimeout: 10000 + timeout: 3000 + retryAttempts: 3 + retryInterval: 1500 + password: ${conf.redis.sentinel.password} + subscriptionsPerConnection: 5 + clientName: null + loadBalancer: ! {} + slaveSubscriptionConnectionMinimumIdleSize: 1 + slaveSubscriptionConnectionPoolSize: 50 + slaveConnectionMinimumIdleSize: 32 + slaveConnectionPoolSize: 64 + masterConnectionMinimumIdleSize: 32 + masterConnectionPoolSize: 64 + readMode: "SLAVE" + sentinelAddresses: ${conf.redis.sentinel.address} + masterName: ${conf.redis.sentinel.masterName} + database: ${conf.redis.sentinel.database} + threads: 0 + nettyThreads: 0 + codec: ! {} + "transportMode": "NIO" \ No newline at end of file diff --git a/chushang-common/chushang-common-redis/src/main/resources/redisson.yml b/chushang-common/chushang-common-redis/src/main/resources/redisson.yml new file mode 100644 index 0000000..39d7e2a --- /dev/null +++ b/chushang-common/chushang-common-redis/src/main/resources/redisson.yml @@ -0,0 +1,38 @@ +spring: + redis: + redisson: + config: | + singleServerConfig: + # 连接空闲超时 如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。 + idleConnectionTimeout: 8000 + # ping超时 + #pingTimeout: 1000 + # 连接超时 + connectTimeout: ${conf.redis.timeout} + # 命令等待超时 + timeout: 3000 + # 命令失败重试次数 + retryAttempts: 3 + # 命令重试发送时间间隔 + retryInterval: 1500 + # 重新连接时间间隔 + #reconnectionTimeout: 3000 + # failedAttempts + #failedAttempts: 3 + # 单个连接最大订阅数量 + subscriptionsPerConnection: 5 + # 客户端名称 + clientName: null + address: "redis://${conf.redis.host}:${conf.redis.port}" + password: ${conf.redis.password} + database: ${conf.redis.database} + subscriptionConnectionMinimumIdleSize: 1 + subscriptionConnectionPoolSize: 50 + connectionMinimumIdleSize: 32 + connectionPoolSize: 64 + # dnsMonitoring: false + # dnsMonitoringInterval: 5000 + threads: 0 + nettyThreads: 0 + codec: ! {} + "transportMode": "NIO" \ No newline at end of file diff --git a/chushang-common/chushang-common-security/pom.xml b/chushang-common/chushang-common-security/pom.xml new file mode 100644 index 0000000..53c2152 --- /dev/null +++ b/chushang-common/chushang-common-security/pom.xml @@ -0,0 +1,32 @@ + + + + chushang-common + com.chushang + 1.0.0 + + 4.0.0 + jar + 1.0.0 + chushang-common-security + + + 17 + 17 + + + + + com.chushang + chushang-common-redis + + + com.chushang + system-feign + 1.0.0 + + + + \ No newline at end of file diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/EnableCustomConfig.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/EnableCustomConfig.java new file mode 100644 index 0000000..58d31a4 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/EnableCustomConfig.java @@ -0,0 +1,24 @@ +package com.chushang.security.annotation; + +import com.chushang.security.config.ApplicationConfig; +import com.chushang.security.feign.FeignAutoConfiguration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; +import org.springframework.scheduling.annotation.EnableAsync; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 开启线程异步执行 +@EnableAsync +// 自动加载类 +@Import({ ApplicationConfig.class, FeignAutoConfiguration.class }) +public @interface EnableCustomConfig +{ + +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/InnerAuth.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/InnerAuth.java new file mode 100644 index 0000000..1bf0c4b --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/InnerAuth.java @@ -0,0 +1,19 @@ +package com.chushang.security.annotation; + +import java.lang.annotation.*; + +/** + * 内部认证注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface InnerAuth +{ + /** + * 是否校验用户信息 + */ + boolean isUser() default false; +} \ No newline at end of file diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/Logical.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/Logical.java new file mode 100644 index 0000000..9e1c774 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/Logical.java @@ -0,0 +1,20 @@ +package com.chushang.security.annotation; + +/** + * 权限注解的验证模式 + * + * @author ruoyi + * + */ +public enum Logical +{ + /** + * 必须具有所有的元素 + */ + AND, + + /** + * 只需具有其中一个元素 + */ + OR +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresLogin.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresLogin.java new file mode 100644 index 0000000..84a84ba --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresLogin.java @@ -0,0 +1,18 @@ +package com.chushang.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 登录认证:只有登录之后才能进入该方法 + * + * @author ruoyi + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresLogin +{ +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresPermissions.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresPermissions.java new file mode 100644 index 0000000..48936fa --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresPermissions.java @@ -0,0 +1,27 @@ +package com.chushang.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 权限认证:必须具有指定权限才能进入该方法 + * + * @author ruoyi + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresPermissions +{ + /** + * 需要校验的权限码 + */ + String[] value() default {}; + + /** + * 验证模式:AND | OR,默认AND + */ + Logical logical() default Logical.AND; +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresRoles.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresRoles.java new file mode 100644 index 0000000..48e77bb --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/annotation/RequiresRoles.java @@ -0,0 +1,27 @@ +package com.chushang.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 角色认证:必须具有指定角色标识才能进入该方法 + * 使用的概率不大 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresRoles +{ + /** + * 需要校验的角色标识 + */ + String[] value() default {}; + + /** + * 验证逻辑:AND | OR,默认AND + */ + Logical logical() default Logical.AND; +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/InnerAuthAspect.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/InnerAuthAspect.java new file mode 100644 index 0000000..7952d5b --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/InnerAuthAspect.java @@ -0,0 +1,51 @@ +package com.chushang.security.aspect; + +import com.chushang.security.annotation.InnerAuth; +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.exception.InnerAuthException; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.core.util.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; + +/** + * 内部服务调用验证处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class InnerAuthAspect implements Ordered +{ + @Around("@annotation(innerAuth)") + public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable + { + String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE); + // 内部请求验证 + if (!StringUtils.equals(SecurityConstants.INNER, source)) + { + throw new InnerAuthException("没有内部访问权限,不允许访问"); + } + + String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID); + String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME); + // 用户信息验证 + if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) + { + throw new InnerAuthException("没有设置用户信息,不允许访问 "); + } + return point.proceed(); + } + + /** + * 确保在权限认证aop执行前执行 + */ + @Override + public int getOrder() + { + return Ordered.HIGHEST_PRECEDENCE + 1; + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/PreAuthorizeAspect.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/PreAuthorizeAspect.java new file mode 100644 index 0000000..d9642bd --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/aspect/PreAuthorizeAspect.java @@ -0,0 +1,90 @@ +package com.chushang.security.aspect; + +import com.chushang.security.annotation.RequiresLogin; +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.annotation.RequiresRoles; +import com.chushang.security.auth.AuthUtil; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 基于 Spring Aop 的注解鉴权 + * + * @author kong + */ +@Aspect +@Component +public class PreAuthorizeAspect +{ + /** + * 构建 + */ + public PreAuthorizeAspect() + { + } + + /** + * 定义AOP签名 (切入所有使用鉴权注解的方法) + */ + public static final String POINTCUT_SIGN = " @annotation(com.chushang.security.annotation.RequiresLogin) || " + + "@annotation(com.chushang.security.annotation.RequiresPermissions) || " + + "@annotation(com.chushang.security.annotation.RequiresRoles)"; + + /** + * 声明AOP签名 + */ + @Pointcut(POINTCUT_SIGN) + public void pointcut() + { + } + + /** + * 环绕切入 + * + * @param joinPoint 切面对象 + * @return 底层方法执行后的返回值 + * @throws Throwable 底层方法抛出的异常 + */ + @Around("pointcut()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable + { + // 注解鉴权 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + checkMethodAnnotation(signature.getMethod()); + // 执行原有逻辑 + return joinPoint.proceed(); + } + + /** + * 对一个Method对象进行注解检查 + */ + public void checkMethodAnnotation(Method method) + { + // 校验 @RequiresLogin 注解 + RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class); + if (requiresLogin != null) + { + AuthUtil.checkLogin(); + } + + // 校验 @RequiresRoles 注解 + RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class); + if (requiresRoles != null) + { + AuthUtil.checkRole(requiresRoles); + } + + // 校验 @RequiresPermissions 注解 + RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class); + if (requiresPermissions != null) + { + AuthUtil.checkPermi(requiresPermissions); + } + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthLogic.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthLogic.java new file mode 100644 index 0000000..f09eb4c --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthLogic.java @@ -0,0 +1,259 @@ +package com.chushang.security.auth; + +import com.chushang.security.annotation.Logical; +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.annotation.RequiresRoles; +import com.chushang.security.context.SecurityContextHolder; +import com.chushang.security.service.TokenService; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.common.core.exception.auth.NotLoginException; +import com.chushang.common.core.exception.auth.NotPermissionException; +import com.chushang.common.core.exception.auth.NotRoleException; +import com.chushang.common.core.util.SpringUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.system.entity.vo.LoginUser; +import org.springframework.util.PatternMatchUtils; + +import java.util.Collection; +import java.util.Set; + +/** + * Token 权限验证,逻辑实现类 + * + * @author ruoyi + */ +public class AuthLogic +{ + /** 所有权限标识 */ + private static final String ALL_PERMISSION = "*:*:*"; + + + public TokenService tokenService = SpringUtils.getBean(TokenService.class); + + + /** + * 会话注销,根据指定Token + */ + public void logoutByToken(String token) + { + tokenService.delLoginUser(token); + } + + /** + * 检验用户是否已经登录,如未登录,则抛出异常 + */ + public void checkLogin() + { + getLoginUser(); + } + + /** + * 获取当前用户缓存信息, 如果未登录,则抛出异常 + * + * @return 用户缓存信息 + */ + public LoginUser getLoginUser() + { + String token = SecurityUtils.getToken(); + if (token == null) + { + throw new NotLoginException("未提供token"); + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser == null) + { + throw new NotLoginException("无效的token"); + } + return loginUser; + } + + /** + * 获取当前用户缓存信息, 如果未登录,则抛出异常 + * + * @param token 前端传递的认证信息 + * @return 用户缓存信息 + */ + public LoginUser getLoginUser(String token) + { + return tokenService.getLoginUser(token); + } + + /** + * 验证当前用户有效期, 如果相差不足120分钟,自动刷新缓存 + * + * @param loginUser 当前用户信息 + */ + public void verifyLoginUserExpire(LoginUser loginUser) + { + tokenService.verifyToken(loginUser); + } + + /** + * 验证用户是否含有指定权限,必须全部拥有 + * + * @param permissions 权限列表 + */ + public void checkPermiAnd(String... permissions) + { + Set permissionList = getPermiList(); + for (String permission : permissions) + { + if (!hasPermi(permissionList, permission)) + { + throw new NotPermissionException(permission); + } + } + } + + /** + * 验证用户是否含有指定权限,只需包含其中一个 + * + * @param permissions 权限码数组 + */ + public void checkPermiOr(String... permissions) + { + Set permissionList = getPermiList(); + for (String permission : permissions) + { + if (hasPermi(permissionList, permission)) + { + return; + } + } + if (permissions.length > 0) + { + throw new NotPermissionException(permissions); + } + } + + /** + * 验证用户是否含有指定角色,必须全部拥有 + * + * @param roles 角色标识数组 + */ + public void checkRoleAnd(String... roles) + { + Set roleList = getRoleList(); + for (String role : roles) + { + if (!hasRole(roleList, role)) + { + throw new NotRoleException(role); + } + } + } + + /** + * 验证用户是否含有指定角色,只需包含其中一个 + * + * @param roles 角色标识数组 + */ + public void checkRoleOr(String... roles) + { + Set roleList = getRoleList(); + for (String role : roles) + { + if (hasRole(roleList, role)) + { + return; + } + } + if (roles.length > 0) + { + throw new NotRoleException(roles); + } + } + + /** + * 根据注解(@RequiresRoles)鉴权 + * + * @param at 注解对象 + */ + public void checkByAnnotation(RequiresRoles at) + { + String[] roleArray = at.value(); + if (at.logical() == Logical.AND) + { + this.checkRoleAnd(roleArray); + } + else + { + this.checkRoleOr(roleArray); + } + } + + /** + * 根据注解(@RequiresPermissions)鉴权 + * + * @param at 注解对象 + */ + public void checkByAnnotation(RequiresPermissions at) + { + String[] permissionArray = at.value(); + if (!isNullOrEmpty(permissionArray)){ + SecurityContextHolder.setPermission(StringUtils.join(permissionArray, ",")); + } + + if (at.logical() == Logical.AND) + { + this.checkPermiAnd(permissionArray); + } + else + { + this.checkPermiOr(permissionArray); + } + } + + public static boolean isNullOrEmpty(String[] array) + { + return null == array || array.length < 1; + } + + /** + * 获取当前账号的角色列表 + * + * @return 角色列表 + */ + public Set getRoleList() + { + LoginUser loginUser = getLoginUser(); + return loginUser.getRoles(); + } + + /** + * 获取当前账号的权限列表 + * + * @return 权限列表 + */ + public Set getPermiList() { + LoginUser loginUser = getLoginUser(); + return loginUser.getPermissions(); + } + + + /** + * 判断是否包含权限 + * + * @param authorities 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(Collection authorities, String permission) + { + return authorities.stream().filter(StringUtils::hasText) + .anyMatch(x -> ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission)); + } + + /** + * 判断是否包含角色 + * + * @param roles 角色列表 + * @param role 角色 + * @return 用户是否具备某角色权限 + */ + public boolean hasRole(Collection roles, String role) + { + return roles.stream().filter(StringUtils::hasText) + .anyMatch(x -> AuthUtil.SUPER_ADMIN.contains(x) || PatternMatchUtils.simpleMatch(x, role)); + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthUtil.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthUtil.java new file mode 100644 index 0000000..dc585ea --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/auth/AuthUtil.java @@ -0,0 +1,71 @@ +package com.chushang.security.auth; + +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.annotation.RequiresRoles; +import com.chushang.system.entity.vo.LoginUser; + +/** + * Token 权限验证工具类 + * + * @author ruoyi + */ +public class AuthUtil +{ + public static final String SUPER_ADMIN = "admin"; + /** + * 底层的 AuthLogic 对象 + */ + public static AuthLogic authLogic = new AuthLogic(); + /** + * 会话注销,根据指定Token + */ + public static void logoutByToken(String token) + { + authLogic.logoutByToken(token); + } + + /** + * 检验当前会话是否已经登录,如未登录,则抛出异常 + */ + public static void checkLogin() + { + authLogic.checkLogin(); + } + + /** + * 获取当前登录用户信息 + */ + public static LoginUser getLoginUser(String token) + { + return authLogic.getLoginUser(token); + } + + /** + * 验证当前用户有效期 + */ + public static void verifyLoginUserExpire(LoginUser loginUser) + { + authLogic.verifyLoginUserExpire(loginUser); + } + + /** + * 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotRoleException + * + * @param requiresRoles 角色权限注解 + */ + public static void checkRole(RequiresRoles requiresRoles) + { + authLogic.checkByAnnotation(requiresRoles); + } + + /** + * 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param requiresPermissions 权限注解 + */ + public static void checkPermi(RequiresPermissions requiresPermissions) + { + authLogic.checkByAnnotation(requiresPermissions); + } + +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/ApplicationConfig.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/ApplicationConfig.java new file mode 100644 index 0000000..99a6ee8 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/ApplicationConfig.java @@ -0,0 +1,23 @@ +package com.chushang.security.config; + +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import java.util.TimeZone; + +/** + * 系统配置 + * + * @author ruoyi + */ +public class ApplicationConfig +{ + /** + * 时区配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() + { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/WebMvcConfig.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/WebMvcConfig.java new file mode 100644 index 0000000..1f9a46e --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/config/WebMvcConfig.java @@ -0,0 +1,63 @@ +package com.chushang.security.config; + +import cn.hutool.core.date.DatePattern; +import com.chushang.security.interceptor.HeaderInterceptor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.time.format.DateTimeFormatter; + +import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET; + +/** + * 拦截器配置 + * + * @author ruoyi + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = SERVLET) +public class WebMvcConfig implements WebMvcConfigurer +{ + /** 不需要拦截地址 */ + public static final String[] excludeUrls = { "/login", "/logout", "/refresh" }; + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(getHeaderInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns(excludeUrls) + .order(-10); + } + + /** + * 自定义请求头拦截器 + */ + public HeaderInterceptor getHeaderInterceptor() + { + return new HeaderInterceptor(); + } + + /** + * 增加GET请求参数中时间类型转换 + *
    + *
  • HH:mm:ss -> LocalTime
  • + *
  • yyyy-MM-dd -> LocalDate
  • + *
  • yyyy-MM-dd HH:mm:ss -> LocalDateTime
  • + *
+ * @param registry + */ + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)); + registrar.setDateFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)); + registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)); + registrar.registerFormatters(registry); + } + +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/context/SecurityContextHolder.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/context/SecurityContextHolder.java new file mode 100644 index 0000000..7560c0e --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/context/SecurityContextHolder.java @@ -0,0 +1,99 @@ +package com.chushang.security.context; + +import com.alibaba.ttl.TransmittableThreadLocal; +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.text.Convert; +import com.chushang.common.core.util.StringUtils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 获取当前线程变量中的 用户id、用户名称、Token等信息 + * 注意: 必须在网关通过请求头的方法传入,同时在HeaderInterceptor拦截器设置值。 否则这里无法获取 + * + * @author ruoyi + */ +public class SecurityContextHolder +{ + private static final TransmittableThreadLocal> THREAD_LOCAL = new TransmittableThreadLocal<>(); + + public static void set(String key, Object value) + { + Map map = getLocalMap(); + map.put(key, value == null ? StringUtils.EMPTY : value); + } + + public static String get(String key) + { + Map map = getLocalMap(); + return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY)); + } + + public static T get(String key, Class clazz) + { + Map map = getLocalMap(); + return StringUtils.cast(map.getOrDefault(key, null)); + } + + public static Map getLocalMap() + { + Map map = THREAD_LOCAL.get(); + if (map == null) + { + map = new ConcurrentHashMap(); + THREAD_LOCAL.set(map); + } + return map; + } + + public static void setLocalMap(Map threadLocalMap) + { + THREAD_LOCAL.set(threadLocalMap); + } + + public static Integer getUserId() + { + return Convert.toInt(get(SecurityConstants.DETAILS_USER_ID), 0); + } + + public static void setUserId(String account) + { + set(SecurityConstants.DETAILS_USER_ID, account); + } + + public static String getUserName() + { + return get(SecurityConstants.DETAILS_USERNAME); + } + + public static void setUserName(String username) + { + set(SecurityConstants.DETAILS_USERNAME, username); + } + + public static String getUserKey() + { + return get(SecurityConstants.USER_KEY); + } + + public static void setUserKey(String userKey) + { + set(SecurityConstants.USER_KEY, userKey); + } + + public static void remove() + { + THREAD_LOCAL.remove(); + } + + public static String getPermission() + { + return get(SecurityConstants.ROLE_PERMISSION); + } + + public static void setPermission(String permissions) + { + set(SecurityConstants.ROLE_PERMISSION, permissions); + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignAutoConfiguration.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignAutoConfiguration.java new file mode 100644 index 0000000..233d236 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignAutoConfiguration.java @@ -0,0 +1,20 @@ +package com.chushang.security.feign; + +import feign.RequestInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Feign 配置注册 + * + * @author ruoyi + **/ +@Configuration +public class FeignAutoConfiguration +{ + @Bean + public RequestInterceptor requestInterceptor() + { + return new FeignRequestInterceptor(); + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignRequestInterceptor.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignRequestInterceptor.java new file mode 100644 index 0000000..8eb2ee4 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/feign/FeignRequestInterceptor.java @@ -0,0 +1,83 @@ +package com.chushang.security.feign; + +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.util.IPUtils; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.security.utils.NonWebRequestAttributes; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Objects; + +/** + * + */ +@Component +public class FeignRequestInterceptor implements RequestInterceptor +{ + @Override + public void apply(RequestTemplate requestTemplate) + { + RequestAttributes requestAttributes = ServletUtils.getRequestAttributes(); + // 不为空时 + if (Objects.nonNull(requestAttributes)){ + if (requestAttributes instanceof ServletRequestAttributes){ + HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); + Map headers = ServletUtils.getHeaders(httpServletRequest); + // 传递用户信息请求头,防止丢失 + String userId = headers.get(SecurityConstants.DETAILS_USER_ID); + if (StringUtils.isNotEmpty(userId)) + { + requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId); + } + String userName = headers.get(SecurityConstants.DETAILS_USERNAME); + if (StringUtils.isNotEmpty(userName)) + { + requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName); + } + String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER); + if (StringUtils.isNotEmpty(authentication)) + { + requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication); + } + // 配置客户端IP + requestTemplate.header("X-Forwarded-For", IPUtils.clientIp(ServletUtils.getRequest())); + } + } + // 此时的情况 应当不为 浏览器调用, 所以请求头可能为空 + else { + RequestContextHolder.setRequestAttributes(new NonWebRequestAttributes(), Boolean.TRUE); + HttpServletRequest httpRequest = this.getHttpServletRequestSafely(); + if (null != httpRequest && null != httpRequest.getAttribute("X-Request-No")) { + requestTemplate.header("X-Request-No", + httpRequest.getAttribute("X-Request-No").toString()); + } + } + } + + public HttpServletRequest getHttpServletRequestSafely() { + try { + RequestAttributes requestAttributesSafely = this.getRequestAttributesSafely(); + return requestAttributesSafely instanceof ServletRequestAttributes ? ((ServletRequestAttributes)requestAttributesSafely).getRequest() : null; + } catch (Exception var2) { + return null; + } + } + + public RequestAttributes getRequestAttributesSafely() { + RequestAttributes requestAttributes; + try { + requestAttributes = RequestContextHolder.currentRequestAttributes(); + } catch (IllegalStateException var3) { + requestAttributes = new NonWebRequestAttributes(); + } + return requestAttributes; + } +} \ No newline at end of file diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/interceptor/HeaderInterceptor.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/interceptor/HeaderInterceptor.java new file mode 100644 index 0000000..75e993c --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/interceptor/HeaderInterceptor.java @@ -0,0 +1,55 @@ +package com.chushang.security.interceptor; + +import com.chushang.security.auth.AuthUtil; +import com.chushang.security.context.SecurityContextHolder; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.system.entity.vo.LoginUser; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 自定义请求头拦截器,将Header数据封装到线程变量中方便获取 + * 注意:此拦截器会同时验证当前用户有效期自动刷新有效期 + * + * @author ruoyi + */ +public class HeaderInterceptor implements AsyncHandlerInterceptor +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (!(handler instanceof HandlerMethod)) + { + return true; + } + + SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID)); + SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME)); + SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY)); + + String token = SecurityUtils.getToken(); + if (StringUtils.isNotEmpty(token)) + { + LoginUser loginUser = AuthUtil.getLoginUser(token); + if (StringUtils.isNotNull(loginUser)) + { + AuthUtil.verifyLoginUserExpire(loginUser); + SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser); + } + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception + { + SecurityContextHolder.remove(); + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/service/TokenService.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/service/TokenService.java new file mode 100644 index 0000000..c407eee --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/service/TokenService.java @@ -0,0 +1,198 @@ +package com.chushang.security.service; + +import com.chushang.common.core.constant.CacheConstants; +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.util.*; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.vo.LoginUser; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RKeys; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +@Slf4j +public class TokenService +{ + @Autowired + private RedissonClient redissonClient; + + // 1秒 + protected static final long MILLIS_SECOND = 1000; + // 1分钟 + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + // 1个小时 + protected static final long MILLIS_HOUR = 60 * MILLIS_MINUTE; + + /** + * 创建令牌 + */ + public Map createToken(LoginUser loginUser) + { + String token = IdUtils.getId(31); + Integer userId = loginUser.getUserId(); + String username = loginUser.getUsername(); + String tokenKey = token + "#" + userId; + loginUser.setToken(tokenKey); + loginUser.setIpaddr(IPUtils.clientIp(ServletUtils.getRequest())); + + refreshToken(loginUser); + + // Jwt存储信息 + Map claimsMap = new HashMap<>(); + claimsMap.put(SecurityConstants.USER_KEY, tokenKey); + claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId); + claimsMap.put(SecurityConstants.DETAILS_USERNAME, username); + + // 接口返回信息 + Map rspMap = new HashMap<>(); + rspMap.put("access_token", JwtUtils.createToken(claimsMap)); + // 默认15天 + rspMap.put("expires_in", 15 * 24 * MILLIS_HOUR ); + return rspMap; + } + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser() + { + return getLoginUser(ServletUtils.getRequest()); + } + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = SecurityUtils.getToken(request); + return getLoginUser(token); + } + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(String token) + { + try + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = JwtUtils.getUserKey(token); + + String tokenKey = getTokenKey(userKey); + + RBucket bucket = redissonClient.getBucket(tokenKey); + + return bucket.get(); + } + } + catch (Exception ignored) + { + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户缓存信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = JwtUtils.getUserKey(token); + redissonClient.getBucket(getTokenKey(userKey)).delete(); + } + } + + /** + * 验证令牌有效期,相差不足 过期时间, 自动刷新缓存 + */ + public void verifyToken(LoginUser loginUser) + { + // 过期时间 + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= 15 * 24 * MILLIS_HOUR) + { + long start = System.currentTimeMillis(); + refreshToken(loginUser); + long end = System.currentTimeMillis(); + log.info("TokenService verifyToken {}", end - start); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) + { + long expireTime = 15 * 24 * MILLIS_HOUR ; + log.info("expireTime : {}", expireTime); + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + + RKeys rKeys = redissonClient.getKeys(); + // 扫描全部的 登录token, 如果登录token 不等于需要刷新的token, 则将此token 进行删除 + Iterable keysByPattern = rKeys.getKeysByPattern(CacheConstants.LOGIN_TOKEN_KEY + "*" + "#" + loginUser.getUserId()); + + for (String key : keysByPattern) { + if (!key.equals(userKey)) { + rKeys.delete(key); + } + } + + redissonClient.getBucket(userKey) + .set(loginUser, expireTime / 1000, TimeUnit.SECONDS); + } + + private String getTokenKey(String token) + { + return CacheConstants.LOGIN_TOKEN_KEY + token; + } + + /** + * 根据 用户ID 进行用户强退 + */ + public void forcedRetreat(Integer userId){ + RKeys rKeys = redissonClient.getKeys(); + long l = rKeys.deleteByPattern(CacheConstants.LOGIN_TOKEN_KEY + "*" + "#" + userId); + if (log.isDebugEnabled()){ + log.debug("强退 {} 个用户, userId : {}.",l, userId ); + } + } +} \ No newline at end of file diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/NonWebRequestAttributes.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/NonWebRequestAttributes.java new file mode 100644 index 0000000..8413324 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/NonWebRequestAttributes.java @@ -0,0 +1,45 @@ +package com.chushang.security.utils; + +import org.springframework.web.context.request.RequestAttributes; + +public class NonWebRequestAttributes implements RequestAttributes { + @Override + public Object getAttribute(String name, int scope) { + return null; + } + + @Override + public void setAttribute(String name, Object value, int scope) { + + } + + @Override + public void removeAttribute(String name, int scope) { + + } + + @Override + public String[] getAttributeNames(int scope) { + return new String[0]; + } + + @Override + public void registerDestructionCallback(String name, Runnable callback, int scope) { + + } + + @Override + public Object resolveReference(String key) { + return null; + } + + @Override + public String getSessionId() { + return null; + } + + @Override + public Object getSessionMutex() { + return null; + } +} diff --git a/chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/SecurityUtils.java b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/SecurityUtils.java new file mode 100644 index 0000000..e72cf73 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/java/com/chushang/security/utils/SecurityUtils.java @@ -0,0 +1,107 @@ +package com.chushang.security.utils; + +import com.chushang.security.context.SecurityContextHolder; +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.constant.TokenConstants; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.security.auth.AuthUtil; +import com.chushang.system.entity.vo.LoginUser; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import javax.servlet.http.HttpServletRequest; +import java.util.Set; + +/** + * 权限获取工具类 + * + * @author ruoyi + */ +public class SecurityUtils +{ + public static final String PA_SA = "%s:%s"; + /** + * 获取用户ID + */ + public static Integer getUserId() + { + return SecurityContextHolder.getUserId(); + } + + public static boolean isAdmin(){ + Integer userId = SecurityContextHolder.getUserId(); + + LoginUser loginUser = getLoginUser(); + Set roles = loginUser.getRoles(); + // 包含 admin 的账号 或者 id == 1 的就是 admin -- 必须是 admin 的才是超管权限 + boolean flag = roles.contains(AuthUtil.SUPER_ADMIN); + + return (userId != null && 1 == userId) || flag; + } + + /** + * 获取用户名称 + */ + public static String getUsername() + { + return SecurityContextHolder.getUserName(); + } + + /** + * 获取登录用户信息 + */ + public static LoginUser getLoginUser() + { + return SecurityContextHolder.get(SecurityConstants.LOGIN_USER, LoginUser.class); + } + + /** + * 获取请求token + */ + public static String getToken() + { + return getToken(ServletUtils.getRequest()); + } + + /** + * 根据request获取请求token + */ + public static String getToken(HttpServletRequest request) + { + // 从header获取token标识 + String token = request.getHeader(TokenConstants.AUTHENTICATION); + return replaceTokenPrefix(token); + } + + /** + * 裁剪token前缀 + */ + public static String replaceTokenPrefix(String token) + { + // 如果前端设置了令牌前缀,则裁剪掉前缀 + if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) + { + token = token.replaceFirst(TokenConstants.PREFIX, ""); + } + return token; + } + + public static String encryptPassword(String password, String salt) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(String.format(PA_SA, password, salt)); + } + + /** + * 判断密码是否相同 + * @param rawPassword 真实密码 + * @param salt 盐 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword,String salt ,String encodedPassword) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(String.format(PA_SA, rawPassword, salt), encodedPassword); + } +} diff --git a/chushang-common/chushang-common-security/src/main/resources/META-INF/spring.factories b/chushang-common/chushang-common-security/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..31b2e94 --- /dev/null +++ b/chushang-common/chushang-common-security/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.chushang.security.config.WebMvcConfig,\ + com.chushang.security.service.TokenService,\ + com.chushang.security.aspect.PreAuthorizeAspect,\ + com.chushang.security.aspect.InnerAuthAspect diff --git a/chushang-common/pom.xml b/chushang-common/pom.xml new file mode 100644 index 0000000..8288dca --- /dev/null +++ b/chushang-common/pom.xml @@ -0,0 +1,31 @@ + + + + chushangcloud + com.chushang + 1.0.0 + + 4.0.0 + + chushang-common + + pom + 公共 + + chushang-common-bom + chushang-common-canal + chushang-common-core + chushang-common-easy-es + chushang-common-excel + chushang-common-feign + chushang-common-job + chushang-common-log + chushang-common-mail + chushang-common-mongo + chushang-common-mybatis + chushang-common-redis + chushang-common-security + + \ No newline at end of file diff --git a/chushang-modules/.idea/compiler.xml b/chushang-modules/.idea/compiler.xml new file mode 100644 index 0000000..e8a56f2 --- /dev/null +++ b/chushang-modules/.idea/compiler.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chushang-modules/.idea/encodings.xml b/chushang-modules/.idea/encodings.xml new file mode 100644 index 0000000..a5cc428 --- /dev/null +++ b/chushang-modules/.idea/encodings.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chushang-modules/.idea/inspectionProfiles/Project_Default.xml b/chushang-modules/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..5f9ae8f --- /dev/null +++ b/chushang-modules/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/chushang-modules/.idea/jarRepositories.xml b/chushang-modules/.idea/jarRepositories.xml new file mode 100644 index 0000000..be5f5c6 --- /dev/null +++ b/chushang-modules/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/chushang-modules/.idea/misc.xml b/chushang-modules/.idea/misc.xml new file mode 100644 index 0000000..4555257 --- /dev/null +++ b/chushang-modules/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/chushang-modules/.idea/uiDesigner.xml b/chushang-modules/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/chushang-modules/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chushang-modules/.idea/vcs.xml b/chushang-modules/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/chushang-modules/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/chushang-modules/.idea/workspace.xml b/chushang-modules/.idea/workspace.xml new file mode 100644 index 0000000..135c14a --- /dev/null +++ b/chushang-modules/.idea/workspace.xml @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660812763153 + + + + + + + + + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-auth/.gitignore b/chushang-modules/chushang-module-auth/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-modules/chushang-module-auth/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-modules/chushang-module-auth/auth-service/pom.xml b/chushang-modules/chushang-module-auth/auth-service/pom.xml new file mode 100644 index 0000000..a011300 --- /dev/null +++ b/chushang-modules/chushang-module-auth/auth-service/pom.xml @@ -0,0 +1,60 @@ + + + + chushang-module-auth + com.chushang + 1.0.0 + + 4.0.0 + + 1.0.0 + auth-service + + + 17 + 17 + + + + org.redisson + redisson-spring-boot-starter + + + com.chushang + system-feign + 1.0.0 + + + com.chushang + chushang-common-security + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + com.chushang + chushang-common-security + 1.0.0 + compile + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/SanyiAuthApplication.java b/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/SanyiAuthApplication.java new file mode 100644 index 0000000..aed3342 --- /dev/null +++ b/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/SanyiAuthApplication.java @@ -0,0 +1,23 @@ +package com.chushang; + +import com.chushang.common.feign.annotation.EnableOnnFeignClients; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 认证授权中心 + * + * @author ruoyi + */ +@EnableDiscoveryClient +@EnableOnnFeignClients +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class }) +public class SanyiAuthApplication +{ + public static void main(String[] args) + { + SpringApplication.run(SanyiAuthApplication.class, args); + } +} diff --git a/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/controller/UserController.java b/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/controller/UserController.java new file mode 100644 index 0000000..c21c792 --- /dev/null +++ b/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/controller/UserController.java @@ -0,0 +1,70 @@ +package com.chushang.auth.controller; + +import com.chushang.auth.service.UserService; +import com.chushang.common.core.util.JwtUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.security.auth.AuthUtil; +import com.chushang.security.service.TokenService; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.bo.LoginBody; +import com.chushang.system.entity.vo.LoginUser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +/** + * token 控制 + * + * @author ruoyi + */ +@Slf4j +@RestController +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RequestMapping(value = "") +public class UserController +{ + final TokenService tokenService; + final UserService userService; + + @PostMapping("login") + public AjaxResult login(@RequestBody LoginBody form) + { + // 用户登录 + LoginUser loginUser = userService.login(form.getUsername(), form.getPassword()); + // 获取登录token + return AjaxResult.success(tokenService.createToken(loginUser)); + } + + @DeleteMapping("logout") + public AjaxResult logout(HttpServletRequest request) + { + String token = SecurityUtils.getToken(request); + log.info("退出登录 {}", token); + if (StringUtils.isNotEmpty(token)) + { + String username = JwtUtils.getUserName(token); + // 删除用户缓存记录 + AuthUtil.logoutByToken(token); + // 记录用户退出日志 + userService.logout(username); + } + return AjaxResult.success(); + } + + @PostMapping("/refresh") + public AjaxResult refresh(HttpServletRequest request) + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + // 刷新令牌有效期 + tokenService.refreshToken(loginUser); + } + return AjaxResult.success(); + } + +} diff --git a/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/service/UserService.java b/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/service/UserService.java new file mode 100644 index 0000000..06c2aea --- /dev/null +++ b/chushang-modules/chushang-module-auth/auth-service/src/main/java/com/chushang/auth/service/UserService.java @@ -0,0 +1,110 @@ +package com.chushang.auth.service; + +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.exception.ServiceException; +import com.chushang.common.core.util.IPUtils; +import com.chushang.common.core.util.IdUtils; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.core.web.Result; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.enums.LoginStatusEnum; +import com.chushang.system.entity.po.SysLoginInfo; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.vo.LoginUser; +import com.chushang.system.feign.RemoteLoginInfoService; +import com.chushang.system.feign.RemoteUserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; + +/** + * by zhaowenyuan create 2022/5/19 16:49 + */ +@Slf4j +@Service +public class UserService { + + @Autowired + RemoteUserService remoteUserService; + @Autowired + RemoteLoginInfoService loginInfoService; + /** + * 登录 + */ + public LoginUser login(String username, String password) + { + // 查询用户信息 + long start = System.currentTimeMillis(); + Result rLoginUser = remoteUserService.getUserInfo(username, SecurityConstants.INNER); + long end = System.currentTimeMillis(); + log.info("time : {}",end - start); + if (Result.FAIL_CODE == rLoginUser.getCode()){ + throw new ServiceException(rLoginUser.getMsg()); + } + + if (ObjectUtils.isEmpty(rLoginUser) || ObjectUtils.isEmpty(rLoginUser.getData()) ){ + recordLoginInfo(username, LoginStatusEnum.LOGIN_FAIL_STATUS, "登录用户不存在"); + throw new ServiceException("登录用户:" + username + " 不存在"); + } + LoginUser loginUser = rLoginUser.getData(); + SysUser sysUser = loginUser.getSysUser(); + Boolean status = sysUser.getStatus(); + if (!status) + { + recordLoginInfo(username, LoginStatusEnum.LOGIN_FAIL_STATUS, "用户已停用,请联系管理员"); + throw new ServiceException("对不起,您的账号:" + username + " 已停用"); + } + // 进行比较了 + if (!SecurityUtils.matchesPassword(password, sysUser.getSalt(), sysUser.getPassword())) + { + recordLoginInfo(username, LoginStatusEnum.LOGIN_FAIL_STATUS, "用户密码错误"); + throw new ServiceException("用户不存在/密码错误"); + } + recordLoginInfo(username, LoginStatusEnum.LOGIN_SUCCESS, "登录成功"); + sysUser.setPassword(""); + sysUser.setSalt(""); + loginUser.setSysUser(sysUser); + return loginUser; + } + + public void logout(String username) { + recordLoginInfo(username, LoginStatusEnum.LOGOUT_SUCCESS, "退出成功"); + } + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param loginStatus 状态 + * @param message 消息内容 + */ + public void recordLoginInfo(String username, LoginStatusEnum loginStatus, String message) + { + SysLoginInfo loginInfo = new SysLoginInfo(); + + HttpServletRequest request = ServletUtils.getRequest(); + //获取request + String ipAddr = IPUtils.clientIp(request); + loginInfo.setUserName(username); + loginInfo.setIpaddr(ipAddr); + loginInfo.setMsg(message); + loginInfo.setStatus(loginStatus); + + try { + loginInfoService.saveLoginInfo(loginInfo, SecurityConstants.INNER); + }catch (Exception e){ + log.error("插入登录日志失败, 失败原因 ", e); + } + } + + public static void main(String[] args) { + String id = IdUtils.getId(10); + System.out.println(id); + String s = SecurityUtils.encryptPassword("12345678", id); + System.out.println(s); + } + +} diff --git a/chushang-modules/chushang-module-auth/auth-service/src/main/resources/bootstrap.yml b/chushang-modules/chushang-module-auth/auth-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..7903935 --- /dev/null +++ b/chushang-modules/chushang-module-auth/auth-service/src/main/resources/bootstrap.yml @@ -0,0 +1,32 @@ +server: + tomcat: + uri-encoding: UTF-8 + threads: + max: 1000 + min-spare: 30 + connection-timeout: 5000ms + port: 8080 + servlet: + context-path: /sanyi/auth +spring: + application: + name: @artifactId@ + cloud: + nacos: + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + group: ${nacos.group} + config: + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + server-addr: ${spring.cloud.nacos.discovery.server-addr} + file-extension: yml + shared-configs: + - dataId: application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + group: ${nacos.group} + profiles: + active: @profiles.active@ + main: + allow-bean-definition-overriding: true +# auth 应当为单独的服务器, sys 为另一单独服务器 diff --git a/chushang-modules/chushang-module-auth/auth-service/src/main/resources/logback-nacos.xml b/chushang-modules/chushang-module-auth/auth-service/src/main/resources/logback-nacos.xml new file mode 100644 index 0000000..b9afaf1 --- /dev/null +++ b/chushang-modules/chushang-module-auth/auth-service/src/main/resources/logback-nacos.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + UTF-8 + + + + + ${log.path}/info.log + + ${log.path}/info.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + info + + + + + ${log.path}/debug.log + + ${log.path}/debug.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + debug + + + + + ${log.path}/error.log + + ${log.path}/error.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chushang-modules/chushang-module-auth/pom.xml b/chushang-modules/chushang-module-auth/pom.xml new file mode 100644 index 0000000..cb7e87d --- /dev/null +++ b/chushang-modules/chushang-module-auth/pom.xml @@ -0,0 +1,23 @@ + + + + chushang-modules + com.chushang + 1.0.0 + + 4.0.0 + + chushang-module-auth + pom + + auth-service + + + + 17 + 17 + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/.gitignore b/chushang-modules/chushang-module-gateway/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-modules/chushang-module-gateway/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-modules/chushang-module-gateway/pom.xml b/chushang-modules/chushang-module-gateway/pom.xml new file mode 100644 index 0000000..545abed --- /dev/null +++ b/chushang-modules/chushang-module-gateway/pom.xml @@ -0,0 +1,85 @@ + + + + chushang-modules + com.chushang + 1.0.0 + + 4.0.0 + + chushang-module-gateway + + 网关 + + + + 17 + 17 + 1.0.0 + + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + org.springframework.boot + spring-boot-starter-data-redis-reactive + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-gateway + + + com.alibaba.csp + sentinel-spring-cloud-gateway-adapter + + + com.github.penggle + kaptcha + + + com.chushang + chushang-common-redis + + + org.springframework.boot + spring-boot-starter-web + + + + + commons-io + commons-io + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/SanyiGatewayApplication.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/SanyiGatewayApplication.java new file mode 100644 index 0000000..3b26fb4 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/SanyiGatewayApplication.java @@ -0,0 +1,19 @@ +package com.chushang; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * by zhaowenyuan create 2022/5/19 11:32 + */ +@EnableDiscoveryClient +@SpringBootApplication +public class SanyiGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(SanyiGatewayApplication.class, args); + } + + +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/CaptchaConfig.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/CaptchaConfig.java new file mode 100644 index 0000000..be8227b --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/CaptchaConfig.java @@ -0,0 +1,85 @@ +package com.chushang.gateway.config; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.gateway.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayConfiguration.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayConfiguration.java new file mode 100644 index 0000000..77c1b6f --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayConfiguration.java @@ -0,0 +1,55 @@ +package com.chushang.gateway.config; + +import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; +import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; +import com.chushang.gateway.handler.SentinelFallbackHandler; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.web.reactive.result.view.ViewResolver; + +import java.util.Collections; +import java.util.List; + +/** + * 网关配置 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +public class GatewayConfiguration { + + private final List viewResolvers; + private final ServerCodecConfigurer serverCodecConfigurer; + + public GatewayConfiguration(ObjectProvider> viewResolversProvider, + ServerCodecConfigurer serverCodecConfigurer) { + this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); + this.serverCodecConfigurer = serverCodecConfigurer; + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { + // Register the block exception handler for Spring Cloud Gateway. + return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public GlobalFilter sentinelGatewayFilter() { + return new SentinelGatewayFilter(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SentinelFallbackHandler sentinelGatewayExceptionHandler() + { + return new SentinelFallbackHandler(); + } + +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayContext.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayContext.java new file mode 100644 index 0000000..c092ad9 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/GatewayContext.java @@ -0,0 +1,23 @@ +package com.chushang.gateway.config; + +import lombok.Data; +import org.springframework.util.MultiValueMap; + +import javax.servlet.http.Part; + +@Data +public class GatewayContext { + public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext"; + /** + * cache json body + */ + private String cacheBody; + /** + * cache form data + */ + private MultiValueMap formData; + /** + * cache request path + */ + private String path; +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/KaptchaTextCreator.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/KaptchaTextCreator.java new file mode 100644 index 0000000..30f94f0 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/KaptchaTextCreator.java @@ -0,0 +1,76 @@ +package com.chushang.gateway.config; + +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +import java.util.Random; + +/** + * 验证码文本生成器 + * + * @author ruoyi + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = (int) Math.round(Math.random() * 2); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if (!(x == 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else if (randomoperands == 2) + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + suChinese.append("=?@").append(result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RateLimiterConfiguration.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RateLimiterConfiguration.java new file mode 100644 index 0000000..b6ccaf0 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RateLimiterConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chushang.gateway.config; + +import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.core.publisher.Mono; + +import java.util.Objects; + +/** + * @author lengleng + * @date 2019/2/1 路由限流配置 + */ +@Configuration(proxyBeanMethods = false) +public class RateLimiterConfiguration { + + /** + * Remote addr key resolver key resolver. + * + * @link {https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-requestratelimiter-gatewayfilter-factory} + */ + @Bean + public KeyResolver remoteAddrKeyResolver() { + return exchange -> Mono + .just(Objects.requireNonNull(Objects.requireNonNull(exchange.getRequest().getRemoteAddress())) + .getAddress().getHostAddress()); + } + +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RouterFunctionConfiguration.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RouterFunctionConfiguration.java new file mode 100644 index 0000000..20194fa --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/RouterFunctionConfiguration.java @@ -0,0 +1,35 @@ +package com.chushang.gateway.config; + +import com.chushang.gateway.handler.ValidateCodeHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; + +/** + * 路由配置信息 + * + * @author ruoyi + */ +@Configuration +public class RouterFunctionConfiguration { + @Autowired + private ValidateCodeHandler validateCodeHandler; + + @SuppressWarnings("rawtypes") + @Bean + public RouterFunction routerFunction() { + return RouterFunctions.route( + RequestPredicates + .GET("/api/sanyi/captcha.jpg") + .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)) + , validateCodeHandler + ); + } + + + +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/pool/PoolUtils.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/pool/PoolUtils.java new file mode 100644 index 0000000..1ef5b20 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/pool/PoolUtils.java @@ -0,0 +1,67 @@ +package com.chushang.gateway.config.pool; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author by zhaowenyuan create 2022/7/26 09:28 + * 线程池 + */ +public enum PoolUtils { + /** + * 线程池 + */ + GATEWAY("GATEWAY-POOL-SERVICE"), + ; + private final ThreadPoolExecutor instance; + + PoolUtils(String threadName){ + this.instance = new ThreadPoolExecutor( + 4, + 5, + 10L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(65535), + new NameThreadFactory(threadName)); + } + + public ThreadPoolExecutor getInstance(){ + return instance; + } + + public void destroy(){ + this.instance.shutdown(); + } + + static class NameThreadFactory implements ThreadFactory{ + private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); + private final ThreadGroup group; + + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + NameThreadFactory(String name){ + SecurityManager securityManager = System.getSecurityManager(); + group = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); + if (null == name || name.isEmpty()){ + name = "pool"; + } + namePrefix = name + "-" + POOL_NUMBER.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread t = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0); + if (t.isDaemon()){ + t.setDaemon(false); + } + if (t.getPriority() != Thread.NORM_PRIORITY){ + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/CaptchaProperties.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/CaptchaProperties.java new file mode 100644 index 0000000..ad80492 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/CaptchaProperties.java @@ -0,0 +1,46 @@ +package com.chushang.gateway.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "security.captcha") +public class CaptchaProperties +{ + /** + * 验证码开关 + */ + private Boolean enabled; + + /** + * 验证码类型(math 数组计算 char 字符) + */ + private String type; + + public Boolean getEnabled() + { + return enabled; + } + + public void setEnabled(Boolean enabled) + { + this.enabled = enabled; + } + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/IgnoreWhiteProperties.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/IgnoreWhiteProperties.java new file mode 100644 index 0000000..89ed4a5 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/IgnoreWhiteProperties.java @@ -0,0 +1,35 @@ +package com.chushang.gateway.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 放行白名单配置 + * + * @author ruoyi + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "security.ignore") +public class IgnoreWhiteProperties +{ + /** + * 放行白名单配置,网关不校验此处的白名单 + */ + private List whites = new ArrayList<>(); + + public List getWhites() + { + return whites; + } + + public void setWhites(List whites) + { + this.whites = whites; + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/XssProperties.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/XssProperties.java new file mode 100644 index 0000000..f33733f --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/config/properties/XssProperties.java @@ -0,0 +1,49 @@ +package com.chushang.gateway.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * XSS跨站脚本配置 + * + * @author ruoyi + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "security.xss") +public class XssProperties +{ + /** + * Xss开关 + */ + private Boolean enabled; + + /** + * 排除路径 + */ + private List excludeUrls = new ArrayList<>(); + + public Boolean getEnabled() + { + return enabled; + } + + public void setEnabled(Boolean enabled) + { + this.enabled = enabled; + } + + public List getExcludeUrls() + { + return excludeUrls; + } + + public void setExcludeUrls(List excludeUrls) + { + this.excludeUrls = excludeUrls; + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ApiLoggingFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ApiLoggingFilter.java new file mode 100644 index 0000000..dcf8d0a --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ApiLoggingFilter.java @@ -0,0 +1,64 @@ +package com.chushang.gateway.filter; + +import com.chushang.common.core.constant.CommonConstants; +import com.chushang.common.core.util.IPUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * @author zhangran + * @date 2021/7/13 + *

+ * 全局拦截器,作用所有的微服务 + *

+ * 1. 对请求的API调用过滤,记录接口的请求时间,方便日志审计、告警、分析等运维操作 2. 后期可以扩展对接其他日志系统 + *

+ */ +@Slf4j +@Component +public class ApiLoggingFilter implements GlobalFilter, Ordered { + + private static final String START_TIME = "startTime"; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + exchange.getAttributes().put(START_TIME, System.currentTimeMillis()); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + if (log.isInfoEnabled()) { + String info = String.format("Method:{%s} Host:{%s} Path:{%s} Query:{%s}", + exchange.getRequest().getMethod().name(), exchange.getRequest().getURI().getHost(), + exchange.getRequest().getURI().getPath(), exchange.getRequest().getQueryParams()); + ServerHttpRequest request = exchange.getRequest(); + String sanyiToken = request.getHeaders().getFirst(CommonConstants.HEAD_TOKEN_KEY); + Long startTime = exchange.getAttribute(START_TIME); + if (startTime != null) { + Long executeTime = (System.currentTimeMillis() - startTime); + String ip = IPUtils.clientIp(request); + // 请求路径 + String api = exchange.getRequest().getURI().getRawPath(); + ServerHttpResponse response = exchange.getResponse(); + HttpStatus statusCode = response.getStatusCode(); + + // 状态码 + int code = statusCode != null ? statusCode.value() : 500; + // 此处 添加 到 plumelog 中, 进行数据统计 + log.info("来自IP地址: {}的请求接口: {}, 请求信息: {},请求人: {} , 响应状态码: {}, 请求耗时: {}ms", ip, api, info, sanyiToken, code, executeTime); + } + } + })); + } + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/AuthFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/AuthFilter.java new file mode 100644 index 0000000..142ebbb --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/AuthFilter.java @@ -0,0 +1,169 @@ +package com.chushang.gateway.filter; + +import cn.hutool.core.util.ObjectUtil; +import com.chushang.common.core.constant.CacheConstants; +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.constant.TokenConstants; +import com.chushang.common.core.util.JwtUtils; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.gateway.config.properties.IgnoreWhiteProperties; +import com.chushang.gateway.config.properties.XssProperties; +import io.jsonwebtoken.Claims; +import org.redisson.api.RedissonClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 网关鉴权 + * + * @author ruoyi + */ +@Component +public class AuthFilter implements GlobalFilter, Ordered +{ + private static final Logger log = LoggerFactory.getLogger(AuthFilter.class); + + // 排除过滤的 uri 地址,nacos自行添加 + @Autowired + private IgnoreWhiteProperties ignoreWhite; + + @Autowired + private RedissonClient redissonClient; + + @Autowired + XssProperties xssProperties; + + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) + { + log.info("AuthFilter start"); + ServerHttpRequest request = exchange.getRequest(); + ServerHttpRequest.Builder mutate = request.mutate(); + + String url = request.getURI().getPath(); + + String newPath = "/" + Arrays.stream(org.springframework.util.StringUtils.tokenizeToStringArray(url, "/")) + .skip(1) + .collect(Collectors.joining("/")); + List whites = ignoreWhite.getWhites(); + // 跳过不需要验证的路径 + if (StringUtils.matches(newPath, whites)) + { + return chain.filter(exchange); + } + long start = System.currentTimeMillis(); + String token = getToken(request, newPath); + if (StringUtils.isEmpty(token)) + { + return unauthorizedResponse(exchange, "令牌不能为空"); + } + Claims claims = JwtUtils.parseToken(token); + if (claims == null) + { + return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); + } + String userkey = JwtUtils.getUserKey(claims); + boolean islogin = redissonClient.getBucket(getTokenKey(userkey)).isExists(); + if (!islogin) + { + return unauthorizedResponse(exchange, "登录状态已过期"); + } + String userid = JwtUtils.getUserId(claims); + String username = JwtUtils.getUserName(claims); + if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) + { + return unauthorizedResponse(exchange, "令牌验证失败"); + } + + // 设置用户信息到请求 + addHeader(mutate, SecurityConstants.USER_KEY, userkey); + addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); + addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); + // 内部请求来源参数清除 + removeHeader(mutate); + + long end = System.currentTimeMillis(); + log.info("auth time {}", end - start); + return chain.filter(exchange.mutate().request(mutate.build()).build()); + } + + private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) + { + if (value == null) + { + return; + } + String valueStr = value.toString(); + String valueEncode = ServletUtils.urlEncode(valueStr); + mutate.header(name, valueEncode); + } + + private void removeHeader(ServerHttpRequest.Builder mutate) + { + mutate.headers(httpHeaders -> httpHeaders.remove(SecurityConstants.FROM_SOURCE)).build(); + } + + private Mono unauthorizedResponse(ServerWebExchange exchange, String msg) + { + log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath()); + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), + msg, + HttpStatus.UNAUTHORIZED.value()); + } + + /** + * 获取缓存key + */ + private String getTokenKey(String token) + { + return CacheConstants.LOGIN_TOKEN_KEY + token; + } + + /** + * 获取请求token + */ + private String getToken(ServerHttpRequest request, String newPath) + { + String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION); + // 如果前端设置了令牌前缀,则裁剪掉前缀 + if (StringUtils.isNotEmpty(token)) { + assert token != null; + if (token.startsWith(TokenConstants.PREFIX)) { + token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY); + } + }else { + token = null; + // 如果为 sanyi/job 时, 通过cookie 获取token + if (StringUtils.matches(newPath, List.of("/sanyi/job/**"))){ + HttpCookie cookie = request.getCookies().getFirst("Admin-Token"); + if (ObjectUtil.isNotEmpty(cookie)){ + assert cookie != null; + token = cookie.getValue(); + } + } + } + return token; + } + + @Override + public int getOrder() + { + return 0; + } +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/BlackListUrlFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/BlackListUrlFilter.java new file mode 100644 index 0000000..48500f4 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/BlackListUrlFilter.java @@ -0,0 +1,69 @@ +package com.chushang.gateway.filter; + +import com.chushang.common.core.util.ServletUtils; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * 黑名单过滤器 + * + * @author ruoyi + */ +@Component +public class BlackListUrlFilter extends AbstractGatewayFilterFactory +{ + @Override + public GatewayFilter apply(Config config) + { + return (exchange, chain) -> { + + String url = exchange.getRequest().getURI().getPath(); + if (config.matchBlacklist(url)) + { + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问"); + } + + return chain.filter(exchange); + }; + } + + public BlackListUrlFilter() + { + super(Config.class); + } + + public static class Config + { + private List blacklistUrl; + + private final List blacklistUrlPattern = new ArrayList<>(); + + public boolean matchBlacklist(String url) + { + return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find()); + } + + public List getBlacklistUrl() + { + return blacklistUrl; + } + + public void setBlacklistUrl(List blacklistUrl) + { + this.blacklistUrl = blacklistUrl; + this.blacklistUrlPattern.clear(); + this.blacklistUrl.forEach(url -> + this.blacklistUrlPattern.add(Pattern.compile( + url.replaceAll("\\*\\*", "(.*?)"), + Pattern.CASE_INSENSITIVE) + ) + ); + } + } + +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/CacheRequestFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/CacheRequestFilter.java new file mode 100644 index 0000000..1bd4650 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/CacheRequestFilter.java @@ -0,0 +1,106 @@ +package com.chushang.gateway.filter; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.List; + +/** + * 获取body请求数据(解决流不能重复读取问题) + * + * @author ruoyi + */ +@Component +public class CacheRequestFilter extends AbstractGatewayFilterFactory +{ + public CacheRequestFilter() + { + super(Config.class); + } + + @Override + public String name() + { + return "CacheRequestFilter"; + } + + @Override + public GatewayFilter apply(Config config) + { + CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter(); + Integer order = config.getOrder(); + if (order == null) + { + return cacheRequestGatewayFilter; + } + return new OrderedGatewayFilter(cacheRequestGatewayFilter, order); + } + + public static class CacheRequestGatewayFilter implements GatewayFilter + { + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) + { + // GET DELETE 不过滤 + HttpMethod method = exchange.getRequest().getMethod(); + if (method == null || method.matches("GET") || method.matches("DELETE")) + { + return chain.filter(exchange); + } + return DataBufferUtils.join(exchange.getRequest().getBody()).map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + return bytes; + }).defaultIfEmpty(new byte[0]).flatMap(bytes -> { + DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory(); + ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) + { + @Override + public Flux getBody() + { + if (bytes.length > 0) + { + return Flux.just(dataBufferFactory.wrap(bytes)); + } + return Flux.empty(); + } + }; + return chain.filter(exchange.mutate().request(decorator).build()); + }); + } + } + + @Override + public List shortcutFieldOrder() + { + return Collections.singletonList("order"); + } + + static class Config + { + private Integer order; + + public Integer getOrder() + { + return order; + } + + public void setOrder(Integer order) + { + this.order = order; + } + } +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiRequestGlobalFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiRequestGlobalFilter.java new file mode 100644 index 0000000..ceaf3b8 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiRequestGlobalFilter.java @@ -0,0 +1,215 @@ +package com.chushang.gateway.filter; + +import com.chushang.gateway.config.GatewayContext; +import io.netty.buffer.ByteBufAllocator; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.NettyDataBufferFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.http.codec.multipart.Part; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * by zhaowenyuan create 2022/6/8 14:27 + * 通用请求过滤 + */ +@Component +@Slf4j +public class SanyiRequestGlobalFilter implements GlobalFilter, Ordered +{ + + /** + * default HttpMessageReader + */ + private static final List> messageReaders = HandlerStrategies.withDefaults().messageReaders(); + + private static final ResolvableType MULTIPART_DATA_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class); + + private static final Mono> EMPTY_MULTIPART_DATA = Mono.just(CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap(0))).cache(); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + if(request.getMethod() == HttpMethod.GET){ + // get请求 处理参数 + return handleGetMethod(exchange, chain, request); + } + + if(request.getMethod() == HttpMethod.POST){ + // post请求 处理参数 + return handlePostMethod(exchange, chain, request); + } + + return chain.filter(exchange); + } + + /** + * get请求 处理参数 + */ + private Mono handleGetMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) { + + return chain.filter(exchange); + } + + private Mono handlePostMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request){ + GatewayContext gatewayContext = new GatewayContext(); + gatewayContext.setPath(request.getPath().pathWithinApplication().value()); + //save gateway context into exchange + exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext); + + MediaType contentType = request.getHeaders().getContentType(); + if(MediaType.APPLICATION_JSON.equals(contentType) + || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){ + // 请求内容为 application json + + // 重新构造 请求体 + return readJsonBody(exchange, chain, gatewayContext); + } +// else +// if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) { +// // 请求内容为 form data +// return readFormData(exchange, chain, gatewayContext); +// } + return chain.filter(exchange); + } + + private Mono readJsonBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) { + return DataBufferUtils.join(exchange.getRequest().getBody()) + .flatMap(dataBuffer -> { + /* + * read the body Flux, and release the buffer + * //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature + * see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095 + */ + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + @SuppressWarnings("ReactiveStreamsUnusedPublisher") Flux cachedFlux = Flux.defer(() -> { + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); + DataBufferUtils.retain(buffer); + return Mono.just(buffer); + }); + ServerHttpRequest mutatedRequest = + new ServerHttpRequestDecorator(exchange.getRequest()) { + @SuppressWarnings("NullableProblems") + @Override + public Flux getBody() { + return cachedFlux; + } + }; + ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build(); + // save body into gatewayContext + return ServerRequest.create(mutatedExchange, messageReaders) + .bodyToMono(String.class) + .doOnNext(gatewayContext::setCacheBody) + .then(chain.filter(mutatedExchange)); + }); + } + + private Mono readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) { + return exchange.getRequest().getBody().collectList().flatMap(dataBuffers -> { + @SuppressWarnings("OptionalGetWithoutIsPresent") final byte[] totalBytes = dataBuffers.stream().map(dataBuffer -> { + try { + return IOUtils.toByteArray(dataBuffer.asInputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).reduce(this::addBytes).get(); + final ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) { + @SuppressWarnings("NullableProblems") + @Override + public Flux getBody() { + return Flux.just(buffer(totalBytes)); + } + }; + final ServerCodecConfigurer configurer = ServerCodecConfigurer.create(); + final Mono> multiValueMapMono = repackageMultipartData(decorator, configurer); + return multiValueMapMono.publishOn(Schedulers.boundedElastic()).publishOn(Schedulers.boundedElastic()) + .publishOn(Schedulers.boundedElastic()).flatMap(part -> { + for (String key : part.keySet()) { + // 如果为文件时 则进入下一次循环 + if (key.equals("file")) { + continue; + } + //noinspection CallingSubscribeInNonBlockingScope + Objects.requireNonNull(part.getFirst(key)).content().subscribe(buffer -> { + final byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); + DataBufferUtils.release(buffer); + final String bodyString = new String(bytes, StandardCharsets.UTF_8); + gatewayContext.setCacheBody(bodyString); + }); + } + return chain.filter(exchange.mutate().request(decorator).build()); + }); + }); + } + + @SuppressWarnings("unchecked") + private static Mono> repackageMultipartData(ServerHttpRequest request, ServerCodecConfigurer configurer) { + try { + final MediaType contentType = request.getHeaders().getContentType(); + if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) { + return ((HttpMessageReader>) configurer.getReaders().stream().filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, MediaType.MULTIPART_FORM_DATA)) + .findFirst().orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader."))).readMono(MULTIPART_DATA_TYPE, request, Collections.emptyMap()) + .switchIfEmpty(EMPTY_MULTIPART_DATA).cache(); + } + } catch (InvalidMediaTypeException ex) { + // Ignore + } + return EMPTY_MULTIPART_DATA; + } + + /** + * addBytes. + * @param first first + * @param second second + * @return byte + */ + public byte[] addBytes(byte[] first, byte[] second) { + final byte[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } + + private DataBuffer buffer(byte[] bytes) { + final NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); + final DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + } + + + @Override + public int getOrder() { + return -1000; + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiResponseGlobalFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiResponseGlobalFilter.java new file mode 100644 index 0000000..fe9f505 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/SanyiResponseGlobalFilter.java @@ -0,0 +1,88 @@ +//package com.chushang.gateway.filter; +// +//import cn.hutool.json.JSONObject; +//import cn.hutool.json.JSONUtil; +//import com.chushang.common.core.web.Result; +//import lombok.extern.slf4j.Slf4j; +//import org.reactivestreams.Publisher; +//import org.springframework.cloud.gateway.filter.GatewayFilterChain; +//import org.springframework.cloud.gateway.filter.GlobalFilter; +//import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage; +//import org.springframework.cloud.gateway.support.BodyInserterContext; +//import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.core.Ordered; +//import org.springframework.core.io.buffer.DataBuffer; +//import org.springframework.core.io.buffer.DataBufferUtils; +//import org.springframework.http.HttpHeaders; +//import org.springframework.http.HttpStatus; +//import org.springframework.http.server.reactive.ServerHttpResponse; +//import org.springframework.http.server.reactive.ServerHttpResponseDecorator; +//import org.springframework.web.reactive.function.BodyInserter; +//import org.springframework.web.reactive.function.BodyInserters; +//import org.springframework.web.reactive.function.client.ClientResponse; +//import org.springframework.web.server.ServerWebExchange; +//import reactor.core.publisher.Flux; +//import reactor.core.publisher.Mono; +// +//@Slf4j +//@Configuration +//public class SanyiResponseGlobalFilter implements GlobalFilter, Ordered { +// +// @Override +// public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { +// ServerHttpResponse originalResponse = exchange.getResponse(); +// ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { +// @Override +// public Mono writeWith(Publisher body) { +// String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); +// HttpHeaders httpHeaders = new HttpHeaders(); +// // explicitly add it in this way instead of +// // 'httpHeaders.setContentType(originalResponseContentType)' +// // this will prevent exception in case of using non-standard media +// // types like "Content-Type: image" +// httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType); +// ClientResponse clientResponse = +// prepareClientResponse(exchange.getResponse().getStatusCode(),body,httpHeaders); +// Mono modifiedBody = clientResponse.bodyToMono(String.class).flatMap(responseBody -> { +// JSONObject jsonObject = JSONUtil.parseObj(responseBody); +// log.info("error : {}", jsonObject); +// if (jsonObject.containsKey("timestamp")) { +// return Mono.just(JSONUtil.toJsonStr(Result.restResult(jsonObject.getStr("path"), +// jsonObject.getInt("status"), +// jsonObject.getStr("error")))); +// } +// return Mono.just(responseBody); +// }); +// BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); +// HttpHeaders headers = new HttpHeaders(); +// headers.putAll(exchange.getResponse().getHeaders()); +// headers.remove(HttpHeaders.CONTENT_LENGTH); +// CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); +// return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { +// Mono messageBody = DataBufferUtils.join(outputMessage.getBody()); +// if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING) +// || headers.containsKey(HttpHeaders.CONTENT_LENGTH)) { +// messageBody = messageBody.doOnNext(data -> headers.setContentLength(data.readableByteCount())); +// } +// // TODO: fail if isStreamingMediaType? +// return getDelegate().writeWith(messageBody); +// })); +// } +// }; +// // replace response with decorator +// return chain.filter(exchange.mutate().response(decoratedResponse).build()); +// } +// +// @Override +// public int getOrder() { +// // -1 is response write filter, must be called before that +// return -2; +// } +// +// private ClientResponse prepareClientResponse(HttpStatus httpStatus, Publisher body, HttpHeaders httpHeaders) { +// ClientResponse.Builder builder; +// builder = ClientResponse.create(httpStatus); +// return builder.headers(headers -> headers.putAll(httpHeaders)).body(Flux.from(body)).build(); +// } +//} \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ValidateCodeFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ValidateCodeFilter.java new file mode 100644 index 0000000..ce38680 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/ValidateCodeFilter.java @@ -0,0 +1,81 @@ +package com.chushang.gateway.filter; + +import cn.hutool.json.JSONUtil; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.util.ServletUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.gateway.config.properties.CaptchaProperties; +import com.chushang.gateway.service.ValidateCodeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; + +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 验证码过滤器 + */ +@Slf4j +@Component +public class ValidateCodeFilter extends AbstractGatewayFilterFactory +{ + private final static String[] VALIDATE_URL = new String[] { "/sanyi/auth/login", "/sanyi/auth/register" }; + + @Autowired + private ValidateCodeService validateCodeService; + + @Autowired + private CaptchaProperties captchaProperties; + + private static final String CODE = "code"; + + private static final String UUID = "uuid"; + + @Override + public GatewayFilter apply(Object config) + { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + // 非登录/注册请求或验证码关闭,不处理 + if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) + { + return chain.filter(exchange); + } + + try + { + String rspStr = resolveBodyFromRequest(request); + cn.hutool.json.JSONObject obj = JSONUtil.parseObj(rspStr); + validateCodeService.checkCaptcha(obj.getStr(CODE), obj.getStr(UUID)); + } + catch (ResultException | ExecutionException | InterruptedException e) + { + log.error("error : ",e); + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage()); + } + return chain.filter(exchange); + }; + } + + private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) + { + // 获取请求体 + Flux body = serverHttpRequest.getBody(); + AtomicReference bodyRef = new AtomicReference<>(); + body.subscribe(buffer -> { + CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()); + DataBufferUtils.release(buffer); + bodyRef.set(charBuffer.toString()); + }); + return bodyRef.get(); + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/XssFilter.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/XssFilter.java new file mode 100644 index 0000000..3349600 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/filter/XssFilter.java @@ -0,0 +1,122 @@ +package com.chushang.gateway.filter; + +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.xss.EscapeUtil; +import com.chushang.gateway.config.properties.XssProperties; +import io.netty.buffer.ByteBufAllocator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.*; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * 跨站脚本过滤器 + * + * @author ruoyi + */ +@Component +@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true") +public class XssFilter implements GlobalFilter, Ordered +{ + // 跨站脚本的 xss 配置,nacos自行添加 + @Autowired + private XssProperties xss; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) + { + ServerHttpRequest request = exchange.getRequest(); + // GET DELETE 不过滤 + HttpMethod method = request.getMethod(); + if (method == null || method.matches("GET") || method.matches("DELETE")) + { + return chain.filter(exchange); + } + // 非json类型,不过滤 + if (!isJsonRequest(exchange)) + { + return chain.filter(exchange); + } + String url = request.getURI().getPath(); + // excludeUrls 不过滤 + String newPath = "/" + Arrays.stream(org.springframework.util.StringUtils.tokenizeToStringArray(url, "/")) + .skip(1) + .collect(Collectors.joining("/")); + if (StringUtils.matches(newPath, xss.getExcludeUrls())) + { + return chain.filter(exchange); + } + ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange); + return chain.filter(exchange.mutate().request(httpRequestDecorator).build()); + + } + + private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange) + { + return new ServerHttpRequestDecorator(exchange.getRequest()) { + @Override + public Flux getBody() { + Flux body = super.getBody(); + return body.buffer().map(dataBuffers -> { + DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); + DataBuffer join = dataBufferFactory.join(dataBuffers); + byte[] content = new byte[join.readableByteCount()]; + join.read(content); + DataBufferUtils.release(join); + String bodyStr = new String(content, StandardCharsets.UTF_8); + // 防xss攻击过滤 + bodyStr = EscapeUtil.clean(bodyStr); + // 转成字节 + byte[] bytes = bodyStr.getBytes(); + NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); + DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + }); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.putAll(super.getHeaders()); + // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length + httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); + httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); + return httpHeaders; + } + + }; + } + + /** + * 是否是Json请求 + * + * @param exchange HTTP请求 + */ + public boolean isJsonRequest(ServerWebExchange exchange) + { + String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } + + @Override + public int getOrder() + { + return -100; + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/GatewayExceptionHandler.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/GatewayExceptionHandler.java new file mode 100644 index 0000000..d700b73 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/GatewayExceptionHandler.java @@ -0,0 +1,56 @@ +package com.chushang.gateway.handler; + +import com.chushang.common.core.util.ServletUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.cloud.gateway.support.NotFoundException; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * 网关统一异常处理 + * + * @author ruoyi + */ +@Order(-1) +@Configuration +public class GatewayExceptionHandler implements ErrorWebExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class); + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) + { + ServerHttpResponse response = exchange.getResponse(); + + if (exchange.getResponse().isCommitted()) + { + return Mono.error(ex); + } + + String msg; + + if (ex instanceof NotFoundException) + { + msg = "服务未找到"; + } + else if (ex instanceof ResponseStatusException) + { + ResponseStatusException responseStatusException = (ResponseStatusException) ex; + msg = responseStatusException.getMessage(); + } + else + { + msg = "内部服务器错误"; + } + + log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex); + + return ServletUtils.webFluxResponseWriter(response, msg); + } +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/SentinelFallbackHandler.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/SentinelFallbackHandler.java new file mode 100644 index 0000000..f7fbcb7 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/SentinelFallbackHandler.java @@ -0,0 +1,41 @@ +package com.chushang.gateway.handler; + +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.chushang.common.core.util.ServletUtils; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebExceptionHandler; +import reactor.core.publisher.Mono; + +/** + * 自定义限流异常处理 + * + * @author ruoyi + */ +public class SentinelFallbackHandler implements WebExceptionHandler +{ + private Mono writeResponse(ServerResponse response, ServerWebExchange exchange) + { + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求超过最大数,请稍候再试"); + } + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) + { + if (exchange.getResponse().isCommitted()) + { + return Mono.error(ex); + } + if (!BlockException.isBlockException(ex)) + { + return Mono.error(ex); + } + return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange)); + } + + private Mono handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) + { + return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/ValidateCodeHandler.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/ValidateCodeHandler.java new file mode 100644 index 0000000..fefdab0 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/handler/ValidateCodeHandler.java @@ -0,0 +1,42 @@ +package com.chushang.gateway.handler; + +import com.chushang.gateway.service.ValidateCodeService; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.web.AjaxResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import java.io.IOException; + +/** + * 验证码获取 + * + * @author ruoyi + */ +@Component +public class ValidateCodeHandler implements HandlerFunction +{ + @Autowired + private ValidateCodeService validateCodeService; + + @Override + public Mono handle(ServerRequest serverRequest) + { + AjaxResult ajax; + try + { + ajax = validateCodeService.createCaptcha(); + } + catch (ResultException | IOException e) + { + return Mono.error(e); + } + return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax)); + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/ValidateCodeService.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/ValidateCodeService.java new file mode 100644 index 0000000..9a886ab --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/ValidateCodeService.java @@ -0,0 +1,25 @@ +package com.chushang.gateway.service; + +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.web.AjaxResult; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +/** + * 验证码处理 + * + * @author ruoyi + */ +public interface ValidateCodeService +{ + /** + * 生成验证码 + */ + AjaxResult createCaptcha() throws IOException, ResultException; + + /** + * 校验验证码 + */ + void checkCaptcha(String key, String value) throws ResultException, ExecutionException, InterruptedException; +} diff --git a/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/impl/ValidateCodeServiceImpl.java b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/impl/ValidateCodeServiceImpl.java new file mode 100644 index 0000000..4fd3a0e --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/java/com/chushang/gateway/service/impl/ValidateCodeServiceImpl.java @@ -0,0 +1,112 @@ +package com.chushang.gateway.service.impl; + +import cn.hutool.core.codec.Base64; +import com.chushang.gateway.service.ValidateCodeService; +import com.google.code.kaptcha.Producer; +import com.chushang.common.core.constant.Constants; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.util.IdUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.gateway.config.properties.CaptchaProperties; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.FastByteArrayOutputStream; + +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * 验证码实现处理 + */ +@Slf4j +@Service +public class ValidateCodeServiceImpl implements ValidateCodeService { + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedissonClient redissonClient; + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 生成验证码 + */ + @Override + public AjaxResult createCaptcha() throws ResultException { + AjaxResult ajax = AjaxResult.success(); + boolean captchaOnOff = captchaProperties.getEnabled(); + ajax.put("captchaOnOff", captchaOnOff); + if (!captchaOnOff) { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.getId(32); + String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; + + String capStr, code = null; + BufferedImage image = null; + + String captchaType = captchaProperties.getType(); + // 生成验证码 + if ("math".equals(captchaType)) { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } else if ("char".equals(captchaType)) { + code = capStr = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + redissonClient.getBucket(verifyKey) + .set(code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try { + assert image != null; + ImageIO.write(image, "jpg", os); + } catch (IOException e) { + return AjaxResult.error(e.getMessage()); + } + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } + + /** + * 校验验证码 + */ + @SneakyThrows + @Override + public void checkCaptcha(String code, String uuid) throws ResultException, ExecutionException, InterruptedException { + if (!"sanyi".equals(code)) { + if (StringUtils.isEmpty(code)) { + throw new ResultException("验证码不能为空"); + } + if (StringUtils.isEmpty(uuid)) { + throw new ResultException("验证码已失效"); + } + String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; + + RBucket bucket = redissonClient.getBucket(verifyKey); + String captcha = bucket.getAndDelete(); + if (!code.equalsIgnoreCase(captcha)) { + throw new ResultException("Verification code error"); + } + } + } +} diff --git a/chushang-modules/chushang-module-gateway/src/main/resources/bootstrap.yml b/chushang-modules/chushang-module-gateway/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..95c3ebf --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/resources/bootstrap.yml @@ -0,0 +1,35 @@ +server: + tomcat: + uri-encoding: UTF-8 + threads: + max: 1000 + min-spare: 30 + connection-timeout: 50000ms + port: 9999 + servlet: + context-path: /api/sanyi +spring: +# codec: +# max-in-memory-size: 5242880 + application: + name: @artifactId@ + cloud: + nacos: + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + group: ${nacos.group} + config: + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + server-addr: ${spring.cloud.nacos.discovery.server-addr} + file-extension: yml + shared-configs: + - dataId: application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + group: ${nacos.group} + profiles: + active: @profiles.active@ + main: + allow-bean-definition-overriding: true + + diff --git a/chushang-modules/chushang-module-gateway/src/main/resources/logback-nacos.xml b/chushang-modules/chushang-module-gateway/src/main/resources/logback-nacos.xml new file mode 100644 index 0000000..01bfca5 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/main/resources/logback-nacos.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + UTF-8 + + + + + ${log.path}/info.log + + ${log.path}/info.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + info + + + + + ${log.path}/debug.log + + ${log.path}/debug.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + debug + + + + + ${log.path}/error.log + + ${log.path}/error.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chushang-modules/chushang-module-gateway/src/test/java/RedisTest.java b/chushang-modules/chushang-module-gateway/src/test/java/RedisTest.java new file mode 100644 index 0000000..03b45c3 --- /dev/null +++ b/chushang-modules/chushang-module-gateway/src/test/java/RedisTest.java @@ -0,0 +1,26 @@ +import com.chushang.SanyiGatewayApplication; +import com.chushang.gateway.service.ValidateCodeService; +import lombok.SneakyThrows; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author by zhaowenyuan create 2022/8/23 12:10 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SanyiGatewayApplication.class) +public class RedisTest { + + @Autowired + private ValidateCodeService validateCodeService; + + @SneakyThrows + @Test + public void test(){ + validateCodeService.checkCaptcha("code", "uuid"); + } + +} diff --git a/chushang-modules/chushang-module-oss/.gitignore b/chushang-modules/chushang-module-oss/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-modules/chushang-module-oss/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-modules/chushang-module-oss/oss-feign/pom.xml b/chushang-modules/chushang-module-oss/oss-feign/pom.xml new file mode 100644 index 0000000..677d4d6 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-feign/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + com.chushang + chushang-module-oss + 1.0.0 + + oss-feign + + + + com.chushang + chushang-common-feign + + + com.chushang + chushang-common-mybatis + + + com.chushang + chushang-common-redis + + + io.minio + minio + + + net.coobird + thumbnailator + + + com.aliyun.oss + aliyun-sdk-oss + + + diff --git a/chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/config/UploadConfig.java b/chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/config/UploadConfig.java new file mode 100644 index 0000000..a9266c4 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/config/UploadConfig.java @@ -0,0 +1,69 @@ +package com.chushang.oss.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import java.io.Serial; +import java.io.Serializable; + +@Configuration +@ConfigurationProperties(prefix = "oss") +@Data +public class UploadConfig implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 获取 domain 路径, 用于返回完整的请求路径 + */ + private String domain; + /** + * 获取临时存储路径, 用于解压缩等 + */ + private String tmpPath; + /** + * ali, minio + */ + private String storage; + /** + * minio url , 阿里云EndPoint + */ + private String endPoint; + /** + * minio accessKey , 阿里云AccessKeyId + */ + private String accessKey; + /** + * minio secretKey, 阿里云AccessKeySecret + */ + private String secretKey; + /** + * 统一前缀 + */ + private String prefix; + private String bucketName; + + @PostConstruct + public void init() { + _domain = domain; + _tmpPath = tmpPath; + } + + private static String _domain; + private static String _tmpPath; + + /** + * 获取 domain 路径, 用于返回完整的请求路径 + */ + public static String getDomain(){ + return _domain; + } + + /** + * 获取临时存储路径, 用于解压缩等 + */ + public static String getTmpPath(){ + return _tmpPath; + } +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/entity/FileSourceInfo.java b/chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/entity/FileSourceInfo.java new file mode 100644 index 0000000..c7352e4 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-feign/src/main/java/com/chushang/oss/entity/FileSourceInfo.java @@ -0,0 +1,41 @@ +package com.chushang.oss.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@TableName("tb_oss_source_info") +@NoArgsConstructor +public class FileSourceInfo { + + public FileSourceInfo(String fid) { + this.fid = fid; + } + + @TableId(value = "fid", type = IdType.INPUT) + private String fid; + @TableField("name") + private String name; + @TableField("mime_type") + private String mimeType; + @TableField("size") + private Long size; + @TableField("md5") + private String md5; + @TableField("path") + private String path; + @TableField("real_path") + private String realPath; + @TableField("upload_ip") + private String uploadIp; + @TableField("storage_region") + private String storageRegion; + @TableField("if_del") + private Integer ifDel; + @TableField(value = "create_time", fill = FieldFill.INSERT) + private LocalDateTime createTime; + +} diff --git a/chushang-modules/chushang-module-oss/oss-service/pom.xml b/chushang-modules/chushang-module-oss/oss-service/pom.xml new file mode 100644 index 0000000..80baf12 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + com.chushang + chushang-module-oss + 1.0.0 + + oss-service + + + + com.chushang + oss-feign + + + com.chushang + chushang-common-security + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/OssApplication.java b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/OssApplication.java new file mode 100644 index 0000000..bb1c04c --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/OssApplication.java @@ -0,0 +1,32 @@ +package com.chushang.oss; + +import com.chushang.common.core.enums.AppStartType; +import com.chushang.common.feign.annotation.EnableOnnFeignClients; +import com.chushang.common.feign.annotation.EnableTransferFeign; +import com.chushang.security.annotation.EnableCustomConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @auther: zhao + * @date: 2024/4/29 10:50 + */ +@EnableDiscoveryClient +@EnableOnnFeignClients +@SpringBootApplication +@EnableCustomConfig +@EnableTransferFeign +public class OssApplication { + + private final static Logger log = LoggerFactory.getLogger(OssApplication.class); + private final static String APP_NAME = "文件管理平台"; + + public static void main(String[] args) { + log.info(AppStartType.START_FORMAT, AppStartType.main.type(), APP_NAME); + SpringApplication.run(OssApplication.class, args); + log.info(AppStartType.END_FORMAT, AppStartType.main.type(), APP_NAME); + } +} diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/mapper/FileSourceMapper.java b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/mapper/FileSourceMapper.java new file mode 100644 index 0000000..cf8d494 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/mapper/FileSourceMapper.java @@ -0,0 +1,11 @@ +package com.chushang.oss.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.oss.entity.FileSourceInfo; + +/** + * @auther: zhao + * @date: 2024/4/29 10:26 + */ +public interface FileSourceMapper extends BaseMapper { +} diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/OssService.java b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/OssService.java new file mode 100644 index 0000000..8a9e6e9 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/OssService.java @@ -0,0 +1,81 @@ +package com.chushang.oss.service; + +import com.chushang.common.core.util.IdUtils; + +import java.io.InputStream; +import java.util.List; + +public interface OssService { + + default String getSuffixPath(String suffix){ + String nanoID = IdUtils.getId(32); + return getSuffixPath(suffix, nanoID); + + } + /** + * 文件路径 + * @param filename 文件名称 + * @param suffix 文件类型 + * @return 返回上传路径 + */ + default String getSuffixPath(String suffix, String filename) { + return filename + suffix; + } + + default String getPrefixPath(String prefix, String filename){ + return prefix + "/" + filename; + } + + default String getSuffixPath(String prefix, String suffix, String filename) + { + //文件路径 + return prefix + "/" + filename + suffix; + } + /** + * 文件上传 + * @param data 文件字节数组 + * @param path 文件路径,包含文件名 + * @return 返回http地址 + */ + String upload(byte[] data, String path); + + /** + * 文件上传 + * @param data 文件字节数组 + * @param suffix 文件类型 + * @return 返回http地址 + */ + String uploadSuffix(byte[] data, String suffix); + + String uploadSuffix(byte[] data, String suffix, String filename); + String uploadPrefix(byte[] data,String prefix, String suffix, String filename); + String uploadPrefix(byte[] data,String prefix, String filename); + + /** + * 文件上传 + * @param inputStream 字节流 + * @param path 文件路径,包含文件名 + * @return 返回http地址 + */ + String upload(InputStream inputStream, String path); + + /** + * 文件上传 + * @param inputStream 字节流 + * @param suffix 文件类型 + * @return 返回http地址 + */ + String uploadSuffix(InputStream inputStream, String suffix); + + /** + * 删除 cloud 中的文件 + * @param path 文件路径 + */ + void delFile(String path); + + /** + * 批量删除 + */ + List delFileBatch(List keys); + +} diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/AliServiceImpl.java b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/AliServiceImpl.java new file mode 100644 index 0000000..f0c7d10 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/AliServiceImpl.java @@ -0,0 +1,132 @@ +package com.chushang.oss.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.model.DeleteObjectsRequest; +import com.aliyun.oss.model.DeleteObjectsResult; +import com.aliyun.oss.model.Payer; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.util.StringUtils; +import com.chushang.oss.config.UploadConfig; +import com.chushang.oss.service.OssService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 阿里云存储 + */ +@Slf4j +@Service +@ConditionalOnExpression("'${oss.storage}'.equals('ali')") +public class AliServiceImpl implements OssService { + + private OSS oss; + + @Resource + private UploadConfig config; +// +// public AliyunUploadStorageService(CloudProperties cloudProperties){ +// this.config = (AliyunProperties) cloudProperties; +// //初始化 +// init(); +// } + + @PostConstruct + private void init(){ + oss = new OSSClientBuilder() + .build(config.getEndPoint(), config.getAccessKey(), config.getSecretKey()); + Payer payer = Payer.Requester; + oss.setBucketRequestPayment(config.getBucketName(), payer); + } + + @Override + public String upload(byte[] data, String path) { + String aliyunPrefix = config.getPrefix(); + if (StringUtils.isNotEmpty(aliyunPrefix)){ + path = aliyunPrefix + "/" + path; + } + return upload(new ByteArrayInputStream(data), path); + } + + /** + * path + * @param inputStream 字节流 + * @param path 文件路径,包含文件名 + */ + @Override + public String upload(InputStream inputStream, String path) { + try { + oss.putObject(config.getBucketName(), path, inputStream); + } catch (Exception e){ + throw new ResultException("上传文件失败,请检查配置信息", e); + } + + return UploadConfig.getDomain() + "/" + path; + } + + @Override + public String uploadSuffix(byte[] data, String suffix) { + return upload(data, getSuffixPath(suffix)); + } + + @Override + public String uploadSuffix(byte[] data, String suffix, String filename) { + return upload(data, getSuffixPath(suffix, filename)); + } + + @Override + public String uploadPrefix(byte[] data, String prefix, String suffix, String filename) { + return upload(data, getSuffixPath(prefix, suffix, filename)); + } + + @Override + public String uploadPrefix(byte[] data, String prefix, String filename) { + return upload(data, getPrefixPath(prefix, filename)); + } + + @Override + public String uploadSuffix(InputStream inputStream, String suffix) { + return upload(inputStream, getSuffixPath(suffix)); + } + + @Override + public void delFile(String path){ + try { + oss.deleteObject(config.getBucketName(), path); + } catch (Exception e){ + throw new ResultException("删除文件失败,请检查配置信息", e); + } + } + + public List delFileBatch(List keys){ + try { + // 批量删除 + DeleteObjectsRequest objectsRequest = new DeleteObjectsRequest(config.getBucketName()); + + objectsRequest.setKeys(keys); + objectsRequest.setQuiet(true); + objectsRequest.setEncodingType("url"); + + DeleteObjectsResult deleteObjectsResult = oss.deleteObjects(objectsRequest); + List deletedObjects = deleteObjectsResult.getDeletedObjects(); + if (CollectionUtil.isNotEmpty(deletedObjects)){ + log.error("以下文件未从oss 中成功删除 : {}", deletedObjects); + return deletedObjects; + } + } catch (Exception e){ + throw new ResultException("删除文件失败,请检查配置信息", e); + } + return new ArrayList<>(); + } + +} + diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/FileSourceService.java b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/FileSourceService.java new file mode 100644 index 0000000..a8d253e --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/FileSourceService.java @@ -0,0 +1,20 @@ +package com.chushang.oss.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @auther: zhao + * @date: 2024/4/29 10:25 + */ +@Slf4j +@Service +public class FileSourceService { + + @Resource + RedissonClient redissonClient; + +} diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/LocalServiceImpl.java b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/LocalServiceImpl.java new file mode 100644 index 0000000..27edb2a --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/LocalServiceImpl.java @@ -0,0 +1,11 @@ +package com.chushang.oss.service.impl; + +import org.springframework.stereotype.Service; + +/** + * @auther: zhao + * @date: 2024/4/28 19:53 + */ +@Service +public class LocalServiceImpl { +} diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/MinioServiceImpl.java b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/MinioServiceImpl.java new file mode 100644 index 0000000..cf69e43 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/java/com/chushang/oss/service/impl/MinioServiceImpl.java @@ -0,0 +1,61 @@ +package com.chushang.oss.service.impl; + +import com.chushang.oss.service.OssService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Service; + +import java.io.InputStream; +import java.util.List; + +/** + * @auther: zhao + * @date: 2024/4/28 19:52 + */ +@Service +@ConditionalOnExpression("'${oss.storage}'.equals('minio')") +public class MinioServiceImpl implements OssService { + @Override + public String upload(byte[] data, String path) { + return ""; + } + + @Override + public String uploadSuffix(byte[] data, String suffix) { + return ""; + } + + @Override + public String uploadSuffix(byte[] data, String suffix, String filename) { + return ""; + } + + @Override + public String uploadPrefix(byte[] data, String prefix, String suffix, String filename) { + return ""; + } + + @Override + public String uploadPrefix(byte[] data, String prefix, String filename) { + return ""; + } + + @Override + public String upload(InputStream inputStream, String path) { + return ""; + } + + @Override + public String uploadSuffix(InputStream inputStream, String suffix) { + return ""; + } + + @Override + public void delFile(String path) { + + } + + @Override + public List delFileBatch(List keys) { + return List.of(); + } +} diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/resources/bootstrap.yml b/chushang-modules/chushang-module-oss/oss-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..8d050a8 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/resources/bootstrap.yml @@ -0,0 +1,31 @@ +server: + tomcat: + uri-encoding: UTF-8 + threads: + max: 1000 + min-spare: 30 + connection-timeout: 5000ms + port: 8085 + servlet: + context-path: /sanyi/oss +spring: + application: + name: @artifactId@ + cloud: + nacos: + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + group: ${nacos.group} + config: + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + server-addr: ${spring.cloud.nacos.discovery.server-addr} + file-extension: yml + shared-configs: + - dataId: application-common.${spring.cloud.nacos.config.file-extension} + group: ${nacos.group} + profiles: + active: @profiles.active@ + main: + allow-bean-definition-overriding: true \ No newline at end of file diff --git a/chushang-modules/chushang-module-oss/oss-service/src/main/resources/logback-nacos.xml b/chushang-modules/chushang-module-oss/oss-service/src/main/resources/logback-nacos.xml new file mode 100644 index 0000000..ac72426 --- /dev/null +++ b/chushang-modules/chushang-module-oss/oss-service/src/main/resources/logback-nacos.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + %boldCyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) TRACE[%X{trace:-}/%X{span:-}] [%highlight(%-5p)][%cyan(%t)][%magenta(%c{50})-%boldMagenta(%M)][%cn] - %msg%n + + UTF-8 + + + + + + ${log.path}/info.log + + ${log.path}/info.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + TRACE[%X{trace:-}/%X{span:-}] %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + info + + + + + ${log.path}/debug.log + + ${log.path}/debug.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + TRACE[%X{trace:-}/%X{span:-}] %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + debug + + + + + ${log.path}/error.log + + ${log.path}/error.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + TRACE[%X{trace:-}/%X{span:-}] %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chushang-modules/chushang-module-oss/pom.xml b/chushang-modules/chushang-module-oss/pom.xml new file mode 100644 index 0000000..411e0ef --- /dev/null +++ b/chushang-modules/chushang-module-oss/pom.xml @@ -0,0 +1,35 @@ + + + + com.chushang + chushang-modules + 1.0.0 + + 1.0.0 + pom + + oss-feign + oss-service + + 4.0.0 + chushang-module-oss + + + 17 + 17 + UTF-8 + + + + + + com.chushang + oss-feign + 1.0.0 + + + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/.gitignore b/chushang-modules/chushang-module-system/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-modules/chushang-module-system/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-modules/chushang-module-system/pom.xml b/chushang-modules/chushang-module-system/pom.xml new file mode 100644 index 0000000..f2254b9 --- /dev/null +++ b/chushang-modules/chushang-module-system/pom.xml @@ -0,0 +1,27 @@ + + + + chushang-modules + com.chushang + 1.0.0 + + 1.0.0 + pom + 4.0.0 + + chushang-module-system + + + 17 + 17 + UTF-8 + + + + system-feign + system-service + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-feign/pom.xml b/chushang-modules/chushang-module-system/system-feign/pom.xml new file mode 100644 index 0000000..a5a72c8 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/pom.xml @@ -0,0 +1,31 @@ + + + + chushang-module-system + com.chushang + 1.0.0 + + 4.0.0 + + system-feign + + + 17 + 17 + UTF-8 + + + + com.chushang + chushang-common-mybatis + ${common.version} + + + com.chushang + chushang-common-feign + ${common.version} + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/CancelUserRole.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/CancelUserRole.java new file mode 100644 index 0000000..f45e3b5 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/CancelUserRole.java @@ -0,0 +1,17 @@ +package com.chushang.system.entity.bo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author by zhaowenyuan create 2022/8/22 12:24 + * 取消授权用户 + */ +@Data +public class CancelUserRole { + @NotNull(message = "role id is null") + private Integer roleId; + @NotNull(message = "user id is null") + private Integer[] userIds; +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/DataAuth.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/DataAuth.java new file mode 100644 index 0000000..2b2c6a5 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/DataAuth.java @@ -0,0 +1,28 @@ +package com.chushang.system.entity.bo; + +import com.chushang.system.entity.enums.AuthTypeEnum; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author by zhaowenyuan create 2022/8/24 10:27 + */ +@Data +public class DataAuth { + /** + * 用户Id + */ + @NotNull(message = "user id is null") + private Integer userId; + /** + * 授权类型 + */ + @NotNull(message = "auth type is null") + private AuthTypeEnum authType; + /** + * 授权数据 + */ + private String authData; + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/LoginBody.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/LoginBody.java new file mode 100644 index 0000000..8a85047 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/LoginBody.java @@ -0,0 +1,19 @@ +package com.chushang.system.entity.bo; + +import lombok.Data; + +/** + * @author by zhaowenyuan create 2022/8/19 11:08 + */ +@Data +public class LoginBody { + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/PasswordForm.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/PasswordForm.java new file mode 100644 index 0000000..2e88dd4 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/PasswordForm.java @@ -0,0 +1,19 @@ +package com.chushang.system.entity.bo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Data +public class PasswordForm { + /** + * 原密码 + */ + private String oldPassword; + /** + * 新密码 + */ + @NotNull(message = "新密码不能为空") + private String newPassword; + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/RoleUser.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/RoleUser.java new file mode 100644 index 0000000..9cedbdd --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/bo/RoleUser.java @@ -0,0 +1,20 @@ +package com.chushang.system.entity.bo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author by zhaowenyuan create 2022/8/22 14:04 + * 用户授权 + */ +@Data +public class RoleUser { + + @NotNull(message = "role id is null") + private Integer roleId; + + @NotNull(message = "user ids is null") + private Integer[] userIds; + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/GitlabUserDTO.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/GitlabUserDTO.java new file mode 100644 index 0000000..49272df --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/GitlabUserDTO.java @@ -0,0 +1,36 @@ +package com.chushang.system.entity.dto; + +import lombok.Data; + +/** + * 用于创建以及修改gitlab 账号使用 + */ +@Data +public class GitlabUserDTO { + /** + * 电子邮件 + */ + private String email; + /** + * 密码随机 + */ + private boolean force_random_password = true; + /** + * 发送用户密码重置链接 + */ + private boolean reset_password = true; + /** + * 跳过确认 + */ + private boolean skip_confirmation = true; + /** + * 姓名 + */ + private String name; + /** + * 用户名 + */ + private String username; + + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListDeptDTO.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListDeptDTO.java new file mode 100644 index 0000000..2d5c7b7 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListDeptDTO.java @@ -0,0 +1,22 @@ +package com.chushang.system.entity.dto; + +import com.chushang.common.core.util.CommonParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author by zhaowenyuan create 2022/8/22 16:45 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ListDeptDTO extends CommonParam { + + private Integer deptId; + + private Integer parentId; + + private String deptName; + + private Boolean status; + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListMenuDTO.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListMenuDTO.java new file mode 100644 index 0000000..cab854c --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListMenuDTO.java @@ -0,0 +1,26 @@ +package com.chushang.system.entity.dto; + +import com.chushang.common.core.util.CommonParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author by zhaowenyuan create 2022/8/22 14:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ListMenuDTO extends CommonParam { + /** + * 菜单名称 + */ + private String menuName; + /** + * 状态 + */ + private boolean status; + /** + * 是否隐藏 + */ + private boolean visible; +} + diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListRoleDTO.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListRoleDTO.java new file mode 100644 index 0000000..9bc666b --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListRoleDTO.java @@ -0,0 +1,17 @@ +package com.chushang.system.entity.dto; + +import com.chushang.common.core.util.CommonParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author by zhaowenyuan create 2022/8/22 11:13 + * 检索 角色 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ListRoleDTO extends CommonParam { + private String roleName; + private String roleKey; + private Boolean status; +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListUserDTO.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListUserDTO.java new file mode 100644 index 0000000..37fa83e --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/dto/ListUserDTO.java @@ -0,0 +1,20 @@ +package com.chushang.system.entity.dto; + +import com.chushang.common.core.util.CommonParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * by zhaowenyuan create 2022/5/20 17:57 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class ListUserDTO extends CommonParam { + + private String username; + private Integer roleId; + /** + * 部门id + */ + private Integer deptId; +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/AuthTypeEnum.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/AuthTypeEnum.java new file mode 100644 index 0000000..7dcab2a --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/AuthTypeEnum.java @@ -0,0 +1,41 @@ +package com.chushang.system.entity.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.baomidou.mybatisplus.annotation.IEnum; +import com.fasterxml.jackson.annotation.JsonValue; +import com.chushang.common.core.web.EnumUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author by zhaowenyuan create 2022/8/24 10:29 + */ +@Getter +@AllArgsConstructor +public enum AuthTypeEnum implements EnumUtils.CodeEnum, IEnum { + APP(1, "appData","项目授权", " select id from sanyi_app "), + PLATFORM(2, "platformData","平台授权", " select id from sanyi_platform "), + /** + * 需要根据角色进行特殊处理的需求 --> 为false 代表不处理, true 时, 代表处理 + */ + SPECIAL(3, "special", "特殊处理", "FALSE"), + ; + + @JsonValue + @EnumValue + private final Integer code; + private final String codeType; + private final String desc; + private final String sql; + + @Override + public Integer getValue() { + return this.code; + } + + @Override + public String getMsg() { + return desc; + } +} + diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/DataTypeEnum.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/DataTypeEnum.java new file mode 100644 index 0000000..b318ad1 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/DataTypeEnum.java @@ -0,0 +1,32 @@ +package com.chushang.system.entity.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.baomidou.mybatisplus.annotation.IEnum; +import com.fasterxml.jackson.annotation.JsonValue; +import com.chushang.common.core.web.EnumUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author by zhaowenyuan create 2022/8/18 18:25 + */ +@AllArgsConstructor +@Getter +public enum DataTypeEnum implements EnumUtils.CodeEnum, IEnum { + BI(1, "bi 平台"), + ; + @EnumValue + @JsonValue + private final Integer code; + private final String desc; + + @Override + public Integer getValue() { + return this.code; + } + + @Override + public String getMsg() { + return this.desc; + } +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/LoginStatusEnum.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/LoginStatusEnum.java new file mode 100644 index 0000000..2e6d668 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/LoginStatusEnum.java @@ -0,0 +1,36 @@ +package com.chushang.system.entity.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.baomidou.mybatisplus.annotation.IEnum; +import com.fasterxml.jackson.annotation.JsonValue; +import com.chushang.common.core.web.EnumUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * by zhaowenyuan create 2022/5/19 18:20 + */ +@Getter +@AllArgsConstructor +public enum LoginStatusEnum implements EnumUtils.CodeEnum, IEnum { + LOGIN_SUCCESS(0, "登录成功"), + LOGOUT_SUCCESS(1, "注销成功"), + REGISTER_SUCCESS(2, "注册成功成功"), + LOGIN_FAIL_STATUS(3, "登录失败"), + ; + + @JsonValue + @EnumValue + private final Integer code; + private final String desc; + + @Override + public Integer getValue() { + return this.code; + } + + @Override + public String getMsg() { + return this.desc; + } +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/MenuTypeEnum.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/MenuTypeEnum.java new file mode 100644 index 0000000..7cb53c0 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/MenuTypeEnum.java @@ -0,0 +1,28 @@ +package com.chushang.system.entity.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.baomidou.mybatisplus.annotation.IEnum; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * by zhaowenyuan create 2022/3/16 11:58 + */ +@Getter +@AllArgsConstructor +public enum MenuTypeEnum implements IEnum { + CATALOG("M","目录"), + MENU("C","菜单"), + BUTTON("F","按钮"), + ; + @EnumValue + @JsonValue + private final String code; + private final String desc; + + @Override + public String getValue() { + return this.code; + } +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/PermTypeEnum.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/PermTypeEnum.java new file mode 100644 index 0000000..cd19cdd --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/enums/PermTypeEnum.java @@ -0,0 +1,40 @@ +package com.chushang.system.entity.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.baomidou.mybatisplus.annotation.IEnum; +import com.fasterxml.jackson.annotation.JsonValue; +import com.chushang.common.core.web.EnumUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author by zhaowenyuan create 2022/8/18 18:15 + * 0 超管-> 查看全部 + * 1 管理员 -> 查看本人及以下 + * 2 本人 -> 查看本人 + * 3 自定义 -> 针对角色就是自由选择菜单, 针对部门就是自由选择数据 + */ +@AllArgsConstructor +@Getter +public enum PermTypeEnum implements EnumUtils.CodeEnum, IEnum { + ALL(0,"超级管理员 -> 查看全部"), + DEPT_AND_CHILD(1,"部门及以下数据权限"), + DEPT(2,"部门"), + CUSTOM(3,"自定义"), + ONESELF(4,"查看本人"), + ; + @EnumValue + @JsonValue + private final Integer code; + private final String desc; + + @Override + public Integer getValue() { + return this.code; + } + + @Override + public String getMsg() { + return this.desc; + } +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDept.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDept.java new file mode 100644 index 0000000..b859df4 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDept.java @@ -0,0 +1,75 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.chushang.common.mybatis.base.BaseEntity; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_dept") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SysDept extends BaseEntity +{ + @Serial + private static final long serialVersionUID = 1L; + + /** 部门ID */ + @TableId(value = "dept_id", type = IdType.AUTO) + private Integer deptId; + + /** 父部门ID */ + @TableField(value = "parent_dept_id") + private Integer parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + @NotBlank(message = "部门名称不能为空") + @Size(max = 30, message = "部门名称长度不能超过30个字符") + private String deptName; + + /** 显示顺序 */ + @NotNull(message = "显示顺序不能为空") + private Integer orderNum; + + /** 部门状态:0停用,1正常 */ + private Boolean status; + + /** 父部门名称 */ + @TableField(exist = false) + private String parentName; + + /** 子部门 */ + @TableField(exist = false) + private List children = new ArrayList<>(); + + /** + * 创建人 + */ + @TableField( + value = "create_by", + updateStrategy = FieldStrategy.NEVER + ) + protected String createBy; + /** + * 修改人 + */ + @TableField( + value = "update_by", + insertStrategy = FieldStrategy.NEVER + ) + protected String updateBy; +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDictData.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDictData.java new file mode 100644 index 0000000..007a88d --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysDictData.java @@ -0,0 +1,67 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_dict_data") +public class SysDictData implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 字典编码 + */ + @TableId(value = "dict_code", type = IdType.AUTO) + private Integer dictCode; + + /** + * 字典排序 + */ + private Integer dictSort; + + /** + * 字典标签 + */ + private String dictLabel; + + /** + * 字典键值 + */ + private String dictValue; + + /** + * 字典类型 + */ + private String dictType; + + /** + * 样式属性(其他样式扩展) + */ + private String cssClass; + + /** + * 表格字典样式 + */ + private String listClass; + + /** + * 是否默认(Y是 N否) + */ + private String isDefault; + + /** + * 状态(0正常 1停用) + */ + private String status; + +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysLoginInfo.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysLoginInfo.java new file mode 100644 index 0000000..8e3b1d0 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysLoginInfo.java @@ -0,0 +1,45 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.chushang.common.mybatis.base.BaseEntity; +import com.chushang.system.entity.enums.LoginStatusEnum; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +/** + * by zhaowenyuan create 2022/5/19 18:18 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_login_info") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SysLoginInfo extends BaseEntity { + + @TableId(value = "info_id", type = IdType.AUTO) + private Long infoId; + + @TableField(value = "username") + private String userName; + + @TableField(value = "status") + private LoginStatusEnum status; + + @TableField(value = "ipaddr") + private String ipaddr; + + @TableField(value = "msg") + private String msg; + + @TableField(value = "access_time") + private LocalDateTime accessTime; + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysMenu.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysMenu.java new file mode 100644 index 0000000..ca86ea1 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysMenu.java @@ -0,0 +1,115 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.chushang.common.mybatis.base.BaseEntity; +import com.chushang.system.entity.enums.MenuTypeEnum; +import lombok.*; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * + *

+ * + * @author author + * @since 2022-08-18 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_menu") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SysMenu extends BaseEntity { + + /** + * 自增主键Id + */ + @TableId(value = "menu_id", type = IdType.AUTO) + private Integer menuId; + /** + * 菜单名称 + */ + private String menuName; + + @TableField(exist = false) + private String parentName; + /** + * 父级菜单id + */ + private Integer parentId; + /** + * 排序,显示用 + */ + private Integer orderNum; + + /** + * 对应的url 路径 + */ + private String path; + + private String component; + + private String query; + + /** + * 是否 外链 true 不为外链, false 为外链 + */ + private boolean frame; + + /** 是否缓存(false不缓存 true缓存) */ + private boolean cache; + + /** + * 是否隐藏 true 隐藏 + */ + private boolean visible; + /** + * 状态 true 停用 + */ + private boolean status; + + /** + * 拥有权限 + */ + private String perms; + + /** + * 图标 + */ + private String icon; + + /** + * 类型 0 目录,1 菜单,2 按钮 + */ + private MenuTypeEnum menuType; + + /** + * 备注 + */ + private String remark; + + /** + * 创建人 + */ + @TableField( + value = "create_by", + updateStrategy = FieldStrategy.NEVER + ) + protected String createBy; + /** + * 修改人 + */ + @TableField( + value = "update_by", + insertStrategy = FieldStrategy.NEVER + ) + protected String updateBy; + + @TableField(exist = false) + private List children = new ArrayList<>(); +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRole.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRole.java new file mode 100644 index 0000000..bb5e9a7 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRole.java @@ -0,0 +1,123 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.chushang.common.mybatis.base.BaseEntity; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +import java.util.Set; + +/** + *

+ * 角色用于控制菜单 不控制用户显示, 用户的显示由部门进行控制 + *

+ * + * @author author + * @since 2022-08-18 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_role") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SysRole extends BaseEntity { + + /** + * 自增主键id + */ + @TableId(value = "role_id", type = IdType.AUTO) + private Integer roleId; + + /** + * 角色名称 + */ + private String roleName; + /** + * 角色权限字符串 + */ + private String roleKey; + + /** + * 默认的 所属部门 --> 为当前创建用户的部门id + */ + private Integer deptId; + + /** + * 排序 + */ + private Integer orderNum; + + /** + 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限) + */ + private String dataScope; + + /** 菜单树选择项是否关联显示( 0, false:父子不互相关联显示 1, true:父子互相关联显示) */ + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + private boolean deptCheckStrictly; + + /** + * 角色状态 + */ + private Boolean status; + + /** + * 备注 + */ + private String remark; + + + /** 用户是否存在此角色标识 默认不存在 */ + @TableField(exist = false) + private boolean flag = false; + + /** 菜单组 */ + @TableField(exist = false) + private Integer[] menuIds; + /** + * 部门组 + */ + @TableField(exist = false) + private Integer[] deptIds; + + @TableField(exist = false) + private Set permissions; + /** + * 创建人 + */ + @TableField( + value = "create_by", + updateStrategy = FieldStrategy.NEVER + ) + protected String createBy; + /** + * 修改人 + */ + @TableField( + value = "update_by", + insertStrategy = FieldStrategy.NEVER + ) + protected String updateBy; + /** + * 是否为超管 + */ + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Integer roleId) + { + return roleId != null && 1 == roleId; + } + + public SysRole(Integer roleId){ + this.roleId = roleId; + } + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleDept.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleDept.java new file mode 100644 index 0000000..b01cfeb --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleDept.java @@ -0,0 +1,41 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author by zhaowenyuan create 2022/8/19 09:56 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_role_dept") +public class SysRoleDept implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 自增Id + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 角色Id + */ + private Integer roleId; + + /** + * 部门id + */ + private Integer deptId; + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleMenu.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleMenu.java new file mode 100644 index 0000000..3a42248 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysRoleMenu.java @@ -0,0 +1,45 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + *

+ * + *

+ * + * @author author + * @since 2022-08-18 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_role_menu") +public class SysRoleMenu implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 自增Id + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 角色Id + */ + private Integer roleId; + + /** + * 菜单id + */ + private Integer menuId; + + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUser.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUser.java new file mode 100644 index 0000000..7ef6103 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUser.java @@ -0,0 +1,113 @@ +package com.chushang.system.entity.po; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.annotation.*; +import com.chushang.common.mybatis.base.BaseEntity; +import lombok.*; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + *

+ * + *

+ * + * @author author + * @since 2022-08-18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +@TableName("sys_user") +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SysUser extends BaseEntity { + + /** + * 自增主键id + */ + @TableId(value = "user_id", type = IdType.AUTO) + private Integer userId; + + /** + * 用户账号 + */ + private String username; + + /** + * 密码 + */ + @TableField(updateStrategy = FieldStrategy.NOT_EMPTY) + private String password; + + /** + * 账号状态 + */ + private Boolean status; + + /** + * 密码盐 + */ + @TableField(updateStrategy = FieldStrategy.NOT_EMPTY) + private String salt; + + /** + * 所属部门id + */ + private Integer deptId; + /** + * 创建人角色 + */ + @TableField(updateStrategy = FieldStrategy.NOT_NULL) + private String createBy; + + /** + * 修改人 + */ + @TableField(updateStrategy = FieldStrategy.NOT_NULL) + private String updateBy; + + @TableField(exist = false) + private SysDept dept; + + @TableField(exist = false) + private List roles; + /** + * 角色组 + */ + @TableField(exist = false) + private Integer[] roleIds; + /** + * 角色id + */ + @TableField(exist = false) + private Integer roleId; + + public boolean isAdmin() + { + return isAdmin(this.userId) || isAdmin(this.roles); + } + + public static boolean isAdmin(Integer userId) + { + return userId != null && 1 == userId; + } + + public static boolean isAdmin(List roles) + { + long admin = 0; + if (CollectionUtil.isNotEmpty(roles)){ + admin = roles.stream().filter(r -> r.getRoleKey().equals("admin")).count(); + } + return admin > 0; + } + public SysUser(Integer userId) + { + this.userId = userId; + } + + + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserData.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserData.java new file mode 100644 index 0000000..246c18a --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserData.java @@ -0,0 +1,37 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.chushang.system.entity.enums.AuthTypeEnum; +import lombok.*; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * @author by zhaowenyuan create 2022/8/24 10:20 + * 用户数据授权 + */ +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder +@TableName(value = "sys_user_data") +public class SysUserData implements Serializable { + + /** 用户ID */ + @TableId(value = "user_id") + private Integer userId; + /** + * 数据类型 + */ + @TableField(value = "data_type") + private AuthTypeEnum dataType; + /** + * 类型对应的值 id 或者其他的唯一标识符号 + */ + @TableField(value = "data_value") + private String dataValue; +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserPost.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserPost.java new file mode 100644 index 0000000..ceaecd3 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserPost.java @@ -0,0 +1,47 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +/** + *

+ * + *

+ * + * @author author + * @since 2022-08-18 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_user_post") +public class SysUserPost implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 自增主键Id + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 岗位Id + */ + private Integer postId; + + /** + * 用户Id + */ + private Integer userId; + + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserRole.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserRole.java new file mode 100644 index 0000000..829a417 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/po/SysUserRole.java @@ -0,0 +1,47 @@ +package com.chushang.system.entity.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +/** + *

+ * + *

+ * + * @author author + * @since 2022-08-18 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_user_role") +public class SysUserRole implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 自增Id + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 角色id + */ + private Integer roleId; + + /** + * 用户id + */ + private Integer userId; + + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/LoginUser.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/LoginUser.java new file mode 100644 index 0000000..e7f4878 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/LoginUser.java @@ -0,0 +1,69 @@ +package com.chushang.system.entity.vo; + +import com.chushang.system.entity.po.SysUser; +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; +import java.util.Set; + +/** + * by zhaowenyuan create 2022/5/19 12:09 + */ +@Data +public class LoginUser implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 用户名id + */ + private Integer userId; + + /** + * 用户名 + */ + private String username; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 角色对象 + */ + private Set roles; + + /** + * 用户信息 + */ + private SysUser sysUser; + + /** + * 当前用户对应的 数据权限 + */ + private Map> authDataMap; + +} + diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/MetaVo.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/MetaVo.java new file mode 100644 index 0000000..5cfa4cd --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/MetaVo.java @@ -0,0 +1,102 @@ +package com.chushang.system.entity.vo; + +import com.chushang.common.core.constant.Constants; +import org.apache.commons.lang3.StringUtils; + +public class MetaVo +{ + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo() + { + } + + public MetaVo(String title, String icon) + { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) + { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if ( StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS)) + { + this.link = link; + } + } + + public boolean isNoCache() + { + return noCache; + } + + public void setNoCache(boolean noCache) + { + this.noCache = noCache; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public String getLink() + { + return link; + } + + public void setLink(String link) + { + this.link = link; + } +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/RouterVo.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/RouterVo.java new file mode 100644 index 0000000..5365015 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/RouterVo.java @@ -0,0 +1,144 @@ +package com.chushang.system.entity.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo +{ + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "ry"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean getHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public Boolean getAlwaysShow() + { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) + { + this.alwaysShow = alwaysShow; + } + + public MetaVo getMeta() + { + return meta; + } + + public void setMeta(MetaVo meta) + { + this.meta = meta; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/TreeSelect.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/TreeSelect.java new file mode 100644 index 0000000..3334468 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/entity/vo/TreeSelect.java @@ -0,0 +1,78 @@ +package com.chushang.system.entity.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.chushang.system.entity.po.SysDept; +import com.chushang.system.entity.po.SysMenu; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Treeselect树结构实体类 + * + * @author ruoyi + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Integer id; + + /** 节点名称 */ + private String label; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Integer getId() + { + return id; + } + + public void setId(Integer id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteAuthDataService.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteAuthDataService.java new file mode 100644 index 0000000..8c96b6e --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteAuthDataService.java @@ -0,0 +1,25 @@ +package com.chushang.system.feign; + +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.constant.ServiceNameConstants; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +/** + * 用于远程调用 授权 + */ +@FeignClient(contextId = "remoteAuthDataService", + value = ServiceNameConstants.SYSTEM_SERVICE_V2, + path = "/sanyi/system/data" +) +public interface RemoteAuthDataService { + + @PostMapping(value = "/remote/app/{appId}") + void authData(@PathVariable(value = "appId") Integer appId, + @SpringQueryMap Integer authType, + @RequestHeader(SecurityConstants.FROM_SOURCE) String source); + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteLoginInfoService.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteLoginInfoService.java new file mode 100644 index 0000000..75eb965 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteLoginInfoService.java @@ -0,0 +1,30 @@ +package com.chushang.system.feign; + +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.constant.ServiceNameConstants; +import com.chushang.common.core.web.Result; +import com.chushang.system.entity.po.SysLoginInfo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +/** + * by zhaowenyuan create 2022/5/20 10:28 + */ +@FeignClient(contextId = "remoteLoginInfoService", + value = ServiceNameConstants.SYSTEM_SERVICE_V2, + path = "/sanyi/system/log" +) +public interface RemoteLoginInfoService { + + /** + * 保存访问记录 + * + * @param source 请求来源 + * @return 结果 + */ + @PostMapping("/login/info") + Result saveLoginInfo(@RequestBody SysLoginInfo sysLogininfo, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteUserService.java b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteUserService.java new file mode 100644 index 0000000..5014c74 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/java/com/chushang/system/feign/RemoteUserService.java @@ -0,0 +1,33 @@ +package com.chushang.system.feign; + +import com.chushang.common.core.constant.SecurityConstants; +import com.chushang.common.core.constant.ServiceNameConstants; +import com.chushang.common.core.web.Result; +import com.chushang.system.entity.vo.LoginUser; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +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.RequestHeader; + +/** + * by zhaowenyuan create 2022/5/20 10:05 + */ +@FeignClient(contextId = "remoteUserService", + value = ServiceNameConstants.SYSTEM_SERVICE_V2, + path = "/sanyi/system/user" +) +public interface RemoteUserService { + + /** + * 通过用户名查询用户信息 + * + * @param username 用户名 + * @param source 请求来源 + * @return 结果 + */ + @GetMapping("/info/{username}") + Result getUserInfo(@PathVariable(value = "username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); + +} diff --git a/chushang-modules/chushang-module-system/system-feign/src/main/resources/META-INF/spring.factories b/chushang-modules/chushang-module-system/system-feign/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..90344de --- /dev/null +++ b/chushang-modules/chushang-module-system/system-feign/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +com.chushang.common.feign.ChushangFeignAutoConfiguration= diff --git a/chushang-modules/chushang-module-system/system-service/pom.xml b/chushang-modules/chushang-module-system/system-service/pom.xml new file mode 100644 index 0000000..8cffcf8 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/pom.xml @@ -0,0 +1,40 @@ + + + + chushang-module-system + com.chushang + 1.0.0 + + 4.0.0 + + system-service + + + 17 + 17 + UTF-8 + + + + + com.chushang + system-feign + 1.0.0 + + + com.chushang + chushang-common-log + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/SystemApplication.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/SystemApplication.java new file mode 100644 index 0000000..5b45c55 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/SystemApplication.java @@ -0,0 +1,22 @@ +package com.chushang.system; + +import com.chushang.common.feign.annotation.EnableOnnFeignClients; +import com.chushang.security.annotation.EnableCustomConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @author by zhaowenyuan create 2022/8/18 17:08 + */ +@EnableDiscoveryClient +@EnableOnnFeignClients +@SpringBootApplication +@EnableCustomConfig +public class SystemApplication { + + public static void main(String[] args) + { + SpringApplication.run(SystemApplication.class, args); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/annotation/DataScope.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/annotation/DataScope.java new file mode 100644 index 0000000..ec16183 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/annotation/DataScope.java @@ -0,0 +1,21 @@ +package com.chushang.system.annotation; + +import java.lang.annotation.*; + +@Target(value = {ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 部门表的别名 + */ + String deptAlias() default ""; + + /** + * 用户表的别名 + */ + String userAlias() default ""; + + String permission() default ""; +} \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/aspect/DataScopeAspect.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/aspect/DataScopeAspect.java new file mode 100644 index 0000000..0cff151 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/aspect/DataScopeAspect.java @@ -0,0 +1,181 @@ +package com.chushang.system.aspect; + +import cn.hutool.core.collection.CollectionUtil; +import com.chushang.common.core.text.Convert; +import com.chushang.common.core.util.CommonParam; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.mybatis.base.BaseEntity; +import com.chushang.security.context.SecurityContextHolder; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.annotation.DataScope; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.vo.LoginUser; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Aspect +@Slf4j +@Component +public class DataScopeAspect { + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + public static final String ROLE_SCOPE = "roleScope"; + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + @Pointcut("@annotation(com.chushang.system.annotation.DataScope)") + public void dataScopePointCut() { + } + + @Before("dataScopePointCut()") + public void doBefore(JoinPoint point) throws Throwable { + log.info("数据权限before 进入"); + clearDataScope(point); + handleDataScope(point); + } + + protected void handleDataScope(final JoinPoint joinPoint) { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) { + SysUser currentUser = loginUser.getSysUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) + && !currentUser.isAdmin() + && !SecurityUtils.isAdmin()) + { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + DataScope dataScope = + method.getAnnotation(DataScope.class); + String permission = StringUtils.defaultIfEmpty(dataScope.permission(), + SecurityContextHolder.getPermission()); + dataScopeFilter(joinPoint, currentUser, dataScope.deptAlias(), + dataScope.userAlias(), permission); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + */ + public void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) { + StringBuilder dataScopeSqlString = new StringBuilder(); + // 根据部门过滤role 显示 + StringBuilder roleSqlString = new StringBuilder(); + List conditions = new ArrayList<>(); + for (SysRole role : user.getRoles()) { + String dataScope = role.getDataScope(); + if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) { + continue; + } + Set permissions = role.getPermissions(); + if (StringUtils.isNotEmpty(permission) && CollectionUtil.isNotEmpty(permissions) + && !CollectionUtil.containsAny(permissions, Convert.toList(permission))) { + continue; + } + if (DATA_SCOPE_ALL.equals(dataScope)) { + dataScopeSqlString = new StringBuilder(); + break; + } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) { + dataScopeSqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, + role.getRoleId())); + } else if (DATA_SCOPE_DEPT.equals(dataScope)) { + dataScopeSqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + + } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) { + dataScopeSqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", + deptAlias, user.getDeptId(), user.getDeptId())); + // 当且仅当用在角色列表页面, 并且角色为 部门及以下 + roleSqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} OR find_in_set( {}, ancestors ) ) ", + "r", user.getDeptId(), user.getDeptId())); + } else if (DATA_SCOPE_SELF.equals(dataScope)) { + if (StringUtils.isNotBlank(userAlias)) { + dataScopeSqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } else { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + dataScopeSqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } + } + conditions.add(dataScope); + } + + if (StringUtils.isNotBlank(dataScopeSqlString.toString())) { + Object params = joinPoint.getArgs()[0]; + String v = " AND (" + dataScopeSqlString.substring(4) + ")"; + Map sqlParam; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { + BaseEntity baseEntity = (BaseEntity) params; + sqlParam = baseEntity.getSqlParam(); + } else if (StringUtils.isNotNull(params) && params instanceof CommonParam) { + CommonParam commonParam = (CommonParam) params; + sqlParam = commonParam.getSqlParam(); + }else { + return; + } + sqlParam.put(DATA_SCOPE, v); + sqlParam.put(ROLE_SCOPE, roleSqlString.toString()); + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) { + Object params = joinPoint.getArgs()[0]; + Map sqlParam; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { + BaseEntity baseEntity = (BaseEntity) params; + sqlParam = baseEntity.getSqlParam(); + } else if (StringUtils.isNotNull(params) && params instanceof CommonParam) { + CommonParam commonParam = (CommonParam) params; + sqlParam = commonParam.getSqlParam(); + }else { + return; + } + sqlParam.put(DATA_SCOPE, ""); + sqlParam.put(ROLE_SCOPE, ""); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DeptController.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DeptController.java new file mode 100644 index 0000000..903d89b --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DeptController.java @@ -0,0 +1,160 @@ +package com.chushang.system.controller; + +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.log.annotation.SysLog; +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.dto.ListDeptDTO; +import com.chushang.system.entity.po.SysDept; +import com.chushang.system.service.ISysDeptService; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.Objects; + +/** + * @author by zhaowenyuan create 2022/8/22 16:43 + * 部门相关 + */ +@RestController +@RequestMapping("/dept") +public class DeptController { + + @Autowired + ISysDeptService sysDeptService; + + /** + * 获取部门列表 + */ + @GetMapping("/list") + @RequiresPermissions("sys:dept:list") + public AjaxResult list(ListDeptDTO listDept) + { + return AjaxResult.success(sysDeptService.selectDeptList(listDept)); + } + + /** + * 查询部门列表(排除节点) + */ + @RequiresPermissions("sys:dept:list") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Integer deptId) + { + List deptList = sysDeptService.selectDeptList(new ListDeptDTO()); + deptList.removeIf(d -> Objects.equals(d.getDeptId(), deptId) + || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); + return AjaxResult.success(deptList); + } + + /** + * 根据部门编号获取详细信息 + */ + @RequiresPermissions("sys:dept:query") + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Integer deptId) + { + sysDeptService.checkDeptDataScope(deptId); + + return AjaxResult.success(sysDeptService.getById(deptId)); + + } + + /** + * 获取部门下拉树列表 + */ + @GetMapping("/tree/select") + public AjaxResult treeSelect(ListDeptDTO listDept) + { + List deptList = sysDeptService.selectDeptList(listDept); + return AjaxResult.success(sysDeptService.buildDeptTreeSelect(deptList)); + } + + /** + * 加载对应角色部门列表树 + */ + @GetMapping(value = "/roleDept/tree/select/{roleId}") + @RequiresPermissions("sys:role:query") + public AjaxResult roleDeptTreeSelect(@PathVariable("roleId") Integer roleId) + { + List deptList = sysDeptService.selectDeptList(new ListDeptDTO()); + return AjaxResult.success() + .add("checkedKeys", sysDeptService.selectDeptListByRoleId(roleId)) + .add("depts", sysDeptService.buildDeptTreeSelect(deptList)); + } + + /** + * 新增部门 + */ + @RequiresPermissions("sys:dept:add") + @SysLog("新增部门") + @PostMapping + public AjaxResult add(@Valid @RequestBody SysDept dept) + { + if (sysDeptService.checkDeptNameUnique(dept)) + { + return AjaxResult.error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + dept.setCreateBy(SecurityUtils.getUsername()); + + sysDeptService.add(dept); + return AjaxResult.success(); + } + + /** + * 修改部门 + */ + @RequiresPermissions("sys:dept:edit") + @SysLog("修改部门") + @PutMapping + public AjaxResult edit(@Valid @RequestBody SysDept dept) + { + Integer deptId = dept.getDeptId(); + + sysDeptService.checkDeptDataScope(deptId); + + if (sysDeptService.checkDeptNameUnique(dept)) + { + return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + else if (dept.getParentId().equals(deptId)) + { + return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); + } + else if (dept.getStatus() && sysDeptService.selectNormalChildrenDeptById(deptId) > 0) + { + return AjaxResult.error("该部门包含未停用的子部门!"); + } + dept.setUpdateBy(SecurityUtils.getUsername()); + + sysDeptService.updateDept(dept); + + return AjaxResult.success(); + } + + /** + * 删除部门 + */ + @RequiresPermissions("sys:dept:remove") + @SysLog("删除部门") + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Integer deptId) + { + if (sysDeptService.hasChildByDeptId(deptId)) + { + return AjaxResult.error("存在下级部门,不允许删除"); + } + if (sysDeptService.checkDeptExistUser(deptId)) + { + return AjaxResult.error("部门存在用户,不允许删除"); + } + sysDeptService.checkDeptDataScope(deptId); + + sysDeptService.removeById(deptId); + return AjaxResult.success(); + } + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DictController.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DictController.java new file mode 100644 index 0000000..27f1aa9 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/DictController.java @@ -0,0 +1,30 @@ +package com.chushang.system.controller; + +import com.chushang.common.core.web.AjaxResult; +import com.chushang.system.service.SysDictDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author by zhaowenyuan create 2022/8/26 11:43 + * 获取字典数据 -- 添加直接入库 + */ +@RestController +@RequestMapping(value = "/dict/data") +public class DictController { + + @Autowired + SysDictDataService dictDataService; + + /** + * 获取字典数据值 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult getInfo(@PathVariable String dictType) + { + return dictDataService.selectDictDataByType(dictType); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/MenuController.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/MenuController.java new file mode 100644 index 0000000..76644da --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/MenuController.java @@ -0,0 +1,161 @@ +package com.chushang.system.controller; + +import com.chushang.common.core.constant.Constants; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.log.annotation.SysLog; +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.dto.ListMenuDTO; +import com.chushang.system.entity.po.SysMenu; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.vo.LoginUser; +import com.chushang.system.service.ISysMenuService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author by zhaowenyuan create 2022/8/22 14:38 + * 菜单控制器 + */ +@RestController +@RequestMapping(value = "/menu") +public class MenuController { + @Autowired + private ISysMenuService menuService; + + /** + * 获取菜单列表 + */ + @RequiresPermissions("sys:menu:list") + @GetMapping("/list") + public AjaxResult list(ListMenuDTO listMenu) + { + Integer userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuList(listMenu, userId); + return AjaxResult.success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @RequiresPermissions("sys:menu:query") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Integer menuId) + { + return AjaxResult.success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @GetMapping("/tree/select") + public AjaxResult treeSelect() + { + Integer userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuList(new ListMenuDTO(), userId); + + return AjaxResult.success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @GetMapping(value = "/roleMenu/tree/select/{roleId}") + public AjaxResult roleMenuTreeSelect(@PathVariable("roleId") Integer roleId) + { + Integer userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuList(userId); + return AjaxResult.success() + .add("checkedKeys", menuService.selectMenuListByRoleId(roleId)) + .add("menus", menuService.buildMenuTreeSelect(menus)); + } + + /** + * 新增菜单 + */ + @RequiresPermissions("sys:menu:add") + @SysLog("新增菜单") + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) + { + if (menuService.checkMenuNameUnique(menu)) + { + return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (!menu.isFrame() && ! StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS)) + { + return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(SecurityUtils.getUsername()); + + menuService.save(menu); + + return AjaxResult.success(); + } + + /** + * 修改菜单 + */ + @RequiresPermissions("sys:menu:edit") + @SysLog("修改菜单") + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) + { + if (menuService.checkMenuNameUnique(menu)) + { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (!menu.isFrame() && !StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS)) + { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (menu.getMenuId().equals(menu.getParentId())) + { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(SecurityUtils.getUsername()); + + menuService.updateById(menu); + return AjaxResult.success(); + } + + + /** + * 删除菜单 + */ + @RequiresPermissions("sys:menu:remove") + @SysLog("删除菜单") + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Integer menuId) + { + if (menuService.hasChildByMenuId(menuId)) + { + return AjaxResult.error("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) + { + return AjaxResult.error("菜单已分配,不允许删除"); + } + + menuService.removeById(menuId); + + return AjaxResult.success(); + } + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("/getRouters") + public AjaxResult getRouters() + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser sysUser = loginUser.getSysUser(); + List menus = menuService.selectMenuTreeByUserId(sysUser); + return AjaxResult.success(menuService.buildMenus(menus)); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/RoleController.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/RoleController.java new file mode 100644 index 0000000..25145f8 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/RoleController.java @@ -0,0 +1,211 @@ +package com.chushang.system.controller; + +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.log.annotation.SysLog; +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.bo.CancelUserRole; +import com.chushang.system.entity.bo.RoleUser; +import com.chushang.system.entity.dto.ListRoleDTO; +import com.chushang.system.entity.dto.ListUserDTO; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.service.ISysRoleService; +import com.chushang.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * @author by zhaowenyuan create 2022/8/22 11:04 + */ +@RestController +@RequestMapping(value = "/role") +public class RoleController { + + @Autowired + ISysRoleService roleService; + @Autowired + ISysUserService userService; + + @RequiresPermissions("sys:role:list") + @GetMapping("/list") + public AjaxResult list(ListRoleDTO listRole) + { + return roleService.selectRoleList(listRole); + } + + /** + * 根据角色编号获取详细信息 + */ + @RequiresPermissions("sys:role:query") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Integer roleId) + { + roleService.checkRoleDataScope(new SysRole(roleId)); + + return AjaxResult.success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @RequiresPermissions("sys:role:add") + @SysLog("新增角色") + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) + { + if (roleService.checkRoleNameUnique(role)) + { + return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (roleService.checkRoleKeyUnique(role)) + { + return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(SecurityUtils.getUsername()); + + roleService.saveRole(role); + + return AjaxResult.success(); + + } + + /** + * 修改保存角色 + */ + @RequiresPermissions("sys:role:edit") + @SysLog("修改角色") + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) + { + if (2 == role.getRoleId()){ + return AjaxResult.success(); + } + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(new SysRole(role.getRoleId())); + if (roleService.checkRoleNameUnique(role)) + { + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (roleService.checkRoleKeyUnique(role)) + { + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(SecurityUtils.getUsername()); + // + roleService.updateRole(role); + + return AjaxResult.success(); + } + + /** + * 修改保存数据权限 + */ + @RequiresPermissions("sys:role:edit") + @SysLog("修改角色权限") + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(new SysRole(role.getRoleId())); + + roleService.authDataScope(role); + + return AjaxResult.success(); + } + + /** + * 状态修改 + */ + @RequiresPermissions("sys:role:edit") + @SysLog("修改角色状态") + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) + { + if (2 == role.getRoleId()){ + return AjaxResult.success(); + } + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(new SysRole(role.getRoleId())); + role.setUpdateBy(SecurityUtils.getUsername()); + roleService.updateRoleStatus(role); + return AjaxResult.success(); + } + + /** + * 删除角色 + */ + @RequiresPermissions("sys:role:remove") + @SysLog("删除角色") + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Integer[] roleIds) + { + roleService.deleteRoleByIds(roleIds); + + return AjaxResult.success(); + } + + /** + * 获取角色选择框列表 + */ + @RequiresPermissions("sys:role:query") + @GetMapping("/option/select") + public AjaxResult optionSelect() + { + return AjaxResult.success(roleService.selectRoleAll(new SysRole())); + } + /** + * 查询已分配用户角色列表 + * 可以根据用户名称查询 + */ + @RequiresPermissions("sys:role:list") + @GetMapping("/authUser/allocatedList") + public AjaxResult allocatedList(@Valid ListUserDTO listUser) + { + return userService.selectAllocatedList(listUser); + } + + /** + * 查询未分配用户角色列表 + */ + @RequiresPermissions("sys:role:list") + @GetMapping("/authUser/unallocatedList") + public AjaxResult unallocatedList(@Valid ListUserDTO listUser) + { + return userService.selectUnallocatedList(listUser); + } + + /** + * 取消授权用户 + */ + @RequiresPermissions("sys:role:edit") + @SysLog("取消授权用户") + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody @Valid CancelUserRole cancelUserRole) + { + roleService.deleteAuthUser(cancelUserRole); + + return AjaxResult.success(); + } + + /** + * 批量选择用户授权 + */ + @RequiresPermissions("sys:role:edit") + @SysLog("用户授权") + @PutMapping("/authUser") + public AjaxResult selectAuthUserAll(@RequestBody @Valid RoleUser roleUser) + { + Integer roleId = roleUser.getRoleId(); + Integer[] userIds = roleUser.getUserIds(); + // 判断当切登录用户有没有 此角色的权限 + roleService.checkRoleDataScope(new SysRole(roleId)); + + roleService.insertAuthUsers(roleId, userIds); + + return AjaxResult.success(); + } + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysLoginInfoController.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysLoginInfoController.java new file mode 100644 index 0000000..84e05f4 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysLoginInfoController.java @@ -0,0 +1,27 @@ +package com.chushang.system.controller; + +import com.chushang.common.core.web.Result; +import com.chushang.security.annotation.InnerAuth; +import com.chushang.system.entity.po.SysLoginInfo; +import com.chushang.system.feign.RemoteLoginInfoService; +import com.chushang.system.service.SysLoginInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/log/login/info") +public class SysLoginInfoController implements RemoteLoginInfoService { + + @Autowired + private SysLoginInfoService sysLoginInfoService; + + @Override + @InnerAuth + @PostMapping + public Result saveLoginInfo(@RequestBody SysLoginInfo sysLogininfo, String source) { + return Result.ok(sysLoginInfoService.saveLoginInfo(sysLogininfo).isSuccess()); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysUserDataController.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysUserDataController.java new file mode 100644 index 0000000..b6c5d45 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/SysUserDataController.java @@ -0,0 +1,77 @@ +package com.chushang.system.controller; + +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.core.web.EnumUtils; +import com.chushang.common.log.annotation.SysLog; +import com.chushang.security.annotation.InnerAuth; +import com.chushang.security.annotation.Logical; +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.bo.DataAuth; +import com.chushang.system.entity.enums.AuthTypeEnum; +import com.chushang.system.feign.RemoteAuthDataService; +import com.chushang.system.service.ISysUserDataService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * @author by zhaowenyuan create 2022/8/24 10:26 + */ +@Slf4j +@RestController +@RequestMapping(value = "/data") +public class SysUserDataController implements RemoteAuthDataService { + + @Autowired + ISysUserDataService userDataService; + + /** + * 针对用户进行授权 + */ + @SysLog(value = "用户数据授权") + @PostMapping(value = "/{userId}/{dataType}") + @RequiresPermissions(value = {"sys:app:auth","sys:platform:auth"}, logical = Logical.OR) + public AjaxResult authData(@RequestBody Map authDataMap, + @PathVariable Integer dataType, + @PathVariable Integer userId) + { + + String authData = authDataMap.get("authData"); + AuthTypeEnum authType = EnumUtils.getByCode(dataType, AuthTypeEnum.class); + if (null == authType){ + throw new ResultException("授权类型不正确"); + } + DataAuth dataAuth = new DataAuth(); + // 格式为 用, 隔开的id + dataAuth.setAuthData(authData); + dataAuth.setUserId(userId); + dataAuth.setAuthType(authType); + + userDataService.authData(dataAuth); + + return AjaxResult.success(); + } + + @GetMapping(value = "/{userId}/{authType}") + @RequiresPermissions(value = {"sys:app:auth","sys:platform:auth"}, logical = Logical.OR) + public AjaxResult getAuthData(@PathVariable Integer userId, + @PathVariable Integer authType) + { + return AjaxResult.success( userDataService.getUserAuthData(userId, authType)); + } + + @Override + @InnerAuth + @PostMapping(value = "/remote/app/{appId}") + public void authData(Integer appId, Integer authType, String source) { + log.info("{}", appId); + // 登录人id + Integer userId = SecurityUtils.getUserId(); + + AuthTypeEnum authTypeEnum = EnumUtils.getByCode(authType, AuthTypeEnum.class); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/UserController.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/UserController.java new file mode 100644 index 0000000..471c395 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/controller/UserController.java @@ -0,0 +1,321 @@ +package com.chushang.system.controller; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.core.web.Result; +import com.chushang.common.log.annotation.SysLog; +import com.chushang.security.annotation.InnerAuth; +import com.chushang.security.annotation.RequiresPermissions; +import com.chushang.security.service.TokenService; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.bo.PasswordForm; +import com.chushang.system.entity.dto.ListUserDTO; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.po.SysUserData; +import com.chushang.system.entity.vo.LoginUser; +import com.chushang.system.feign.RemoteUserService; +import com.chushang.system.service.ISysPermissionService; +import com.chushang.system.service.ISysRoleService; +import com.chushang.system.service.ISysUserDataService; +import com.chushang.system.service.ISysUserService; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author by zhaowenyuan create 2022/8/18 17:50 + */ +@RestController +@RequestMapping(value = "/user") +public class UserController implements RemoteUserService { + + @Autowired + ISysUserService sysUserService; + @Autowired + ISysRoleService sysRoleService; + @Autowired + ISysPermissionService permissionService; + @Autowired + ISysUserDataService userDataService; + @Autowired + TokenService tokenService; + + /** + * 用户列表 + */ + @GetMapping(value = "/list") + // 权限校验 -- 菜单权限 + @RequiresPermissions("sys:user:list") + public AjaxResult listUser(ListUserDTO listUserDTO) { + return sysUserService.listUser(listUserDTO); + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("info") + public AjaxResult getInfo() { + Integer userId = SecurityUtils.getUserId(); + SysUser sysUser = sysUserService.selectUserById(userId); + // 角色集合 + Set roles = permissionService.getRolePermission(sysUser); + // 权限集合 + Set permissions = permissionService.getMenuPermission(sysUser); + + AjaxResult ajax = AjaxResult.success(); + sysUser.setPassword(""); + sysUser.setSalt(""); + ajax.put("user", sysUser); + ajax.put("roles", roles); + ajax.put("permissions", permissions); + return ajax; + } + + /** + * 获取个人信息 + */ + @GetMapping("/profile") + public AjaxResult profile() + { + String username = SecurityUtils.getUsername(); + SysUser user = sysUserService.selectUserByUserName(username); + return AjaxResult.success(user) + .add("roleGroup", sysUserService.selectUserRoleGroup(username)); + } + + /** + * 修改用户 + */ + @SysLog("修改信息登录名称") + @PutMapping("/profile") + public AjaxResult updateProfile(@RequestBody SysUser user) + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + String username = user.getUsername(); + if (!sysUserService.checkUsername(username)){ + SysUser sysUser = loginUser.getSysUser(); + user.setUserId(sysUser.getUserId()); + user.setUsername(username); + user.setPassword(null); + user.setDeptId(null); + + if (sysUserService.updateUserProfile(user)) + { + // 更新缓存用户信息 + tokenService.setLoginUser(loginUser); + return AjaxResult.success(); + } + } + return AjaxResult.error("修改个人信息异常,请联系管理员"); + } + + /** + * 根据用户编号获取详细信息 + */ + @RequiresPermissions("sys:user:query") + @GetMapping(value = {"/", "/{userId}"}) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Integer userId) { + sysUserService.checkUserDataScope(new SysUser(userId)); + AjaxResult ajax = AjaxResult.success(); + List roles = sysRoleService.selectRoleAll(new SysRole()); + // 角色 + ajax.put("roles", SysUser.isAdmin(userId) || SecurityUtils.isAdmin() ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + if (StringUtils.isNotNull(userId)) { + SysUser sysUser = sysUserService.selectUserById(userId); + sysUser.setPassword(""); + sysUser.setSalt(""); + ajax.put(AjaxResult.DATA_TAG, sysUser); + ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); + } + return ajax; + } + + @SysLog("保存用户") + @PostMapping("/save") + @RequiresPermissions(value = "sys:user:save") + public AjaxResult save(@RequestBody SysUser user) { + + user.setCreateBy(SecurityUtils.getUsername()); + + sysUserService.saveUser(user); + + return AjaxResult.success(); + } + + + /** + * 修改用户 + */ + @SysLog("修改用户") + @PostMapping("/update") + @RequiresPermissions("sys:user:update") + public AjaxResult update(@RequestBody SysUser user) { + + sysUserService.checkUserAllowed(user); + + sysUserService.checkUserDataScope(new SysUser(user.getUserId())); + + sysUserService.update(user); + + return AjaxResult.success(); + } + + /** + * 删除用户 + */ + @SysLog("删除用户") + @DeleteMapping("/{userIds}") + @RequiresPermissions("sys:user:delete") + public AjaxResult delete(@PathVariable Integer[] userIds) { + if (ArrayUtils.contains(userIds, 1)) { + return AjaxResult.error("系统管理员不能删除"); + } + + if (ArrayUtils.contains(userIds, SecurityUtils.getUserId())) { + return AjaxResult.error("当前用户不能删除"); + } + + sysUserService.deleteBatch(userIds); + + return AjaxResult.success(); + } + + /** + * 修改自己的 + */ + @RequiresPermissions("sys:user:updatePwd") + @SysLog("修改密码") + @PutMapping("/updatePwd") + public AjaxResult updatePwd(@RequestBody @Valid PasswordForm form) + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser sysUser = loginUser.getSysUser(); + + if (!SecurityUtils.matchesPassword(form.getOldPassword(), sysUser.getSalt(), sysUser.getPassword())){ + throw new ResultException("原密码不正确"); + } + //sha256加密 + String newPassword = SecurityUtils.encryptPassword(form.getNewPassword(), sysUser.getSalt()); + //更新密码 + boolean flag = sysUserService.updatePassword(sysUser.getUserId(), sysUser.getPassword(), newPassword); + if(!flag){ + throw new ResultException("原密码不正确"); + } + + // 更新缓存用户密码 + loginUser.getSysUser().setPassword(newPassword); + tokenService.setLoginUser(loginUser); + return AjaxResult.success(); + } + + /** + * 重置密码 别人的 + */ + @RequiresPermissions("sys:user:resetPwd") + @SysLog("重置密码") + @PutMapping("/resetPwd/{userId}") + public AjaxResult resetPwd(@RequestBody @Valid PasswordForm form, + @PathVariable Integer userId) + { + // 不允许操作超级管理账号 + sysUserService.checkUserAllowed(new SysUser(userId)); + // 查看自己有没有权限操作 + sysUserService.checkUserDataScope(new SysUser(SecurityUtils.getUserId())); + + sysUserService.resetPwd(userId, form.getNewPassword()); + + return AjaxResult.success(); + } + + /** + * 状态修改 + */ + @RequiresPermissions("sys:user:changeStatus") + @SysLog("修改用户状态") + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) + { + sysUserService.checkUserAllowed(user); + sysUserService.checkUserDataScope(new SysUser(user.getUserId())); + user.setUpdateBy(SecurityUtils.getUsername()); + sysUserService.updateUserStatus(user); + return AjaxResult.success(); + } + + /** + * 根据用户编号获取授权角色 + */ + @RequiresPermissions("sys:user:query") + @GetMapping("/authRole/{userId}") + public AjaxResult authRole(@PathVariable Integer userId) + { + AjaxResult ajax = AjaxResult.success(); + SysUser user = sysUserService.selectUserById(userId); + // 此处获取的 role 应当为 当前用户所能显示的所有role + List roles = sysRoleService.selectRolesByUserId(user); + user.setPassword(""); + user.setSalt(""); + ajax.put("user", user); + ajax.put("roles", SysUser.isAdmin(userId) || SecurityUtils.isAdmin() ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return ajax; + } + + /** + * 用户授权角色 + */ + @RequiresPermissions("sys:user:auth") + @SysLog("用户授权") + @PutMapping("/authRole") + public AjaxResult insertAuthRole(Integer userId, Integer[] roleIds) + { + sysUserService.checkUserDataScope(new SysUser(userId)); + sysUserService.insertUserAuth(userId, roleIds); + return AjaxResult.success(); + } + + @InnerAuth + @GetMapping("/info/{username}") + public Result getUserInfo(@PathVariable("username") String username, String source) + { + SysUser sysUser = sysUserService.selectUserByUserName(username); + if (StringUtils.isNull(sysUser)) { + return Result.failed("用户名或密码错误"); + } + + // 角色集合 + Set roles = permissionService.getRolePermission(sysUser); + // 权限集合 + Set permissions = permissionService.getMenuPermission(sysUser); + + LoginUser sysUserVo = new LoginUser(); + sysUserVo.setUserId(sysUser.getUserId()); + sysUserVo.setUsername(sysUser.getUsername()); + sysUserVo.setSysUser(sysUser); + sysUserVo.setRoles(roles); + sysUserVo.setPermissions(permissions); + + List userDataList = userDataService.listUserData(sysUser); + // 默认传递一个空Map --> 拥有管理员 + sysUserVo.setAuthDataMap(new HashMap<>()); + if (CollectionUtil.isNotEmpty(userDataList)){ + Map> authDataMap = new HashMap<>(); + // 对应的数据权限 + for (SysUserData sysUserData : userDataList) { + authDataMap.put(sysUserData.getDataType().getCodeType(), + new HashSet<>(JSONUtil.toList(sysUserData.getDataValue(), String.class))); + } + sysUserVo.setAuthDataMap(authDataMap); + } + return Result.ok(sysUserVo); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDeptMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDeptMapper.java new file mode 100644 index 0000000..9924325 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDeptMapper.java @@ -0,0 +1,26 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.dto.ListDeptDTO; +import com.chushang.system.entity.po.SysDept; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * 部门用于控制各平台的数据权限 Mapper 接口 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface SysDeptMapper extends BaseMapper { + + List selectDeptList(ListDeptDTO listDept); + + List selectDeptListByRoleId(@Param("roleId") Integer roleId, + @Param("deptCheckStrictly") boolean deptCheckStrictly); + + void updateDeptChildren(@Param("depts") List depts); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDictDataMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDictDataMapper.java new file mode 100644 index 0000000..ee33bbc --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysDictDataMapper.java @@ -0,0 +1,10 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.po.SysDictData; + +/** + * @author by zhaowenyuan create 2022/8/26 11:40 + */ +public interface SysDictDataMapper extends BaseMapper { +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysLoginInfoMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysLoginInfoMapper.java new file mode 100644 index 0000000..e517767 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysLoginInfoMapper.java @@ -0,0 +1,7 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.po.SysLoginInfo; + +public interface SysLoginInfoMapper extends BaseMapper { +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysMenuMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..1abd753 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysMenuMapper.java @@ -0,0 +1,36 @@ +package com.chushang.system.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.dto.ListMenuDTO; +import com.chushang.system.entity.po.SysMenu; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +/** + *

+ * Mapper 接口 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface SysMenuMapper extends BaseMapper { + Set selectMenuPermsByUserId(Integer userId); + + List selectMenuList(ListMenuDTO menu); + + List selectMenuListByUserId(ListMenuDTO menu); + + List selectMenuListByRoleId(@Param("roleId") Integer roleId, + @Param("menuCheckStrictly") boolean menuCheckStrictly); + + List selectMenuTreeAll(); + + + List selectMenuTreeByUserId(Integer userId); + + Set selectMenuPermsByRoleId(@Param("roleId") Integer roleId); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleDeptMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..05e5914 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,16 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.po.SysRoleDept; + +/** + *

+ * Mapper 接口 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface SysRoleDeptMapper extends BaseMapper { + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..fd0b171 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMapper.java @@ -0,0 +1,30 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.chushang.system.entity.dto.ListRoleDTO; +import com.chushang.system.entity.po.SysRole; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +/** + *

+ * 角色用于控制菜单 及 用户的显示 Mapper 接口 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface SysRoleMapper extends BaseMapper { + + List selectRoleList(SysRole sysRole); + + List listRole(@Param("listRole") ListRoleDTO listRole, + Page page); + + SysRole selectRoleById(Integer roleId); + + Set selectRoleByUsername(String username); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMenuMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..1c8f253 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,16 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.po.SysRoleMenu; + +/** + *

+ * Mapper 接口 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface SysRoleMenuMapper extends BaseMapper { + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserDataMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserDataMapper.java new file mode 100644 index 0000000..98df416 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserDataMapper.java @@ -0,0 +1,10 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.po.SysUserData; + +/** + * @author by zhaowenyuan create 2022/9/14 10:24 + */ +public interface SysUserDataMapper extends BaseMapper { +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..09bc0f5 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserMapper.java @@ -0,0 +1,39 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.chushang.system.entity.dto.ListUserDTO; +import com.chushang.system.entity.po.SysUser; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * Mapper 接口 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface SysUserMapper extends BaseMapper { + + SysUser selectUserByUserName(String username); + + SysUser selectUserById(Integer userId); + + + List selectUserList(SysUser user); + + + List listUser(@Param("listUser") ListUserDTO listUser, + Page page); + + + List selectAllocatedList(@Param("listUser") ListUserDTO listUser, + Page page); + + + List selectUnallocatedList(@Param("listUser") ListUserDTO listUser, + Page page); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserPostMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..75b79d6 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserPostMapper.java @@ -0,0 +1,10 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.po.SysUserPost; + +/** + * @author by zhaowenyuan create 2022/8/19 10:01 + */ +public interface SysUserPostMapper extends BaseMapper { +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserRoleMapper.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..38e1733 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,16 @@ +package com.chushang.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.chushang.system.entity.po.SysUserRole; + +/** + *

+ * Mapper 接口 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface SysUserRoleMapper extends BaseMapper { + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysDeptService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysDeptService.java new file mode 100644 index 0000000..35ced91 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysDeptService.java @@ -0,0 +1,122 @@ +package com.chushang.system.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.common.core.exception.ServiceException; +import com.chushang.common.core.text.Convert; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.mybatis.enums.Operator; +import com.chushang.system.entity.dto.ListDeptDTO; +import com.chushang.system.entity.po.SysDept; +import com.chushang.system.entity.vo.TreeSelect; + +import java.util.List; + +/** + *

+ * 部门用于控制各平台的数据权限 服务类 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface ISysDeptService extends IService { + + List selectDeptList(ListDeptDTO listDept); + + void checkDeptDataScope(Integer deptId); + + List buildDeptTreeSelect(List deptList); + + List buildDeptTree(List deptList); + + List selectDeptListByRoleId(Integer roleId); + + default boolean checkDeptNameUnique(SysDept dept){ + int deptId = StringUtils.isNull(dept.getDeptId()) ? -1 : dept.getDeptId(); + SysDept info = getOne(new LambdaQueryWrapper() + .eq(SysDept::getDeptName, dept.getDeptName()) + .eq(SysDept::getParentId, dept.getParentId()) + .last(Operator.LIMIT_ONE.getCharacter())); + return StringUtils.isNotNull(info) && info.getDeptId() != deptId; + } + + default long selectNormalChildrenDeptById(Integer deptId){ + return count(new LambdaQueryWrapper() + .eq(SysDept::getStatus, Boolean.FALSE) + .apply("find_in_set({0}, ancestors)", deptId)); + } + + default boolean hasChildByDeptId(Integer deptId){ + return count(new LambdaQueryWrapper() + .eq(SysDept::getParentId, deptId) + ) > 0; + } + + boolean checkDeptExistUser(Integer deptId); + + default void add(SysDept dept){ + SysDept info = getById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!info.getStatus()) + { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + save(dept); + } + + default void updateDept(SysDept dept){ + SysDept newParentDept = getById(dept.getParentId()); + SysDept oldDept = getById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) + { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + + updateById(dept); + + if (dept.getStatus() && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) + { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + } + + default void updateParentDeptStatusNormal(SysDept dept) + { + String ancestors = dept.getAncestors(); + Integer[] deptIds = Convert.toIntArray(ancestors); + updateDeptStatusNormal(deptIds); + } + + default void updateDeptStatusNormal(Integer[] deptIds) { + SysDept sysDept = new SysDept(); + sysDept.setStatus(true); + update(sysDept, new LambdaQueryWrapper() + .in(SysDept::getDeptId, List.of(deptIds))); + } + + default void updateDeptChildren(Integer deptId, String newAncestors, String oldAncestors){ + List children = selectChildrenDeptById(deptId); + for (SysDept child : children) + { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) + { + updateDeptChildren(children); + } + } + + default List selectChildrenDeptById(Integer deptId){ + return list(new LambdaQueryWrapper() + .apply(" find_in_set({0}, ancestors)", deptId)); + } + + void updateDeptChildren(List children); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysMenuService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysMenuService.java new file mode 100644 index 0000000..1e15c36 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysMenuService.java @@ -0,0 +1,63 @@ +package com.chushang.system.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.mybatis.enums.Operator; +import com.chushang.system.entity.dto.ListMenuDTO; +import com.chushang.system.entity.po.SysMenu; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.vo.RouterVo; +import com.chushang.system.entity.vo.TreeSelect; + +import java.util.List; +import java.util.Set; + +/** + *

+ * 服务类 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface ISysMenuService extends IService { + + Set selectMenuPermsByUserId(Integer userId); + + List selectMenuList(ListMenuDTO menu, Integer userId); + + default List selectMenuList(Integer userId){ + return selectMenuList(new ListMenuDTO(), userId); + } + + default SysMenu selectMenuById(Integer menuId){ + return getById(menuId); + } + + List buildMenuTreeSelect(List menus); + + List selectMenuListByRoleId(Integer roleId); + + default boolean checkMenuNameUnique(SysMenu menu){ + int menuId = StringUtils.isNull(menu.getMenuId()) ? -1 : menu.getMenuId(); + SysMenu info = getOne(new LambdaQueryWrapper() + .eq(SysMenu::getMenuName, menu.getMenuName()) + .eq(SysMenu::getParentId, menu.getParentId()) + .last(Operator.LIMIT_ONE.getCharacter())); + return StringUtils.isNotNull(info) && info.getMenuId() != menuId; + } + + default boolean hasChildByMenuId(Integer menuId){ + return count(new LambdaQueryWrapper() + .eq(SysMenu::getParentId, menuId)) > 0; + } + + boolean checkMenuExistRole(Integer menuId); + + List selectMenuTreeByUserId(SysUser sysUser); + + List buildMenus(List menus); + + Set selectMenuPermsByRoleId(Integer roleId); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysPermissionService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysPermissionService.java new file mode 100644 index 0000000..263ec76 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysPermissionService.java @@ -0,0 +1,14 @@ +package com.chushang.system.service; + +import com.chushang.system.entity.po.SysUser; + +import java.util.Set; + +/** + * @author by zhaowenyuan create 2022/8/19 09:43 + */ +public interface ISysPermissionService { + Set getRolePermission(SysUser sysUser); + + Set getMenuPermission(SysUser sysUser); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleDeptService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleDeptService.java new file mode 100644 index 0000000..d3ac5f4 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleDeptService.java @@ -0,0 +1,49 @@ +package com.chushang.system.service; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.system.entity.po.SysRoleDept; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * 服务类 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface ISysRoleDeptService extends IService { + + default void saveBatch(Integer roleId, Integer[] deptIds){ + + remove(new LambdaQueryWrapper().eq(SysRoleDept::getRoleId, roleId)); + + // 新增角色与部门(数据权限)管理 + List list = new ArrayList<>(); + for (Integer deptId : deptIds) + { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(roleId); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) + { + saveBatch(list); + } + } + + default void deleteRoleDept(List roleIds){ + if (CollectionUtil.isNotEmpty(roleIds)){ + roleIds.forEach(this::removeByRoleId); + } + } + + default void removeByRoleId(Integer roleId){ + remove(new LambdaQueryWrapper().eq(SysRoleDept::getRoleId, roleId)); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleMenuService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleMenuService.java new file mode 100644 index 0000000..a748abc --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleMenuService.java @@ -0,0 +1,52 @@ +package com.chushang.system.service; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.system.entity.po.SysRoleMenu; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + *

+ * 服务类 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface ISysRoleMenuService extends IService { + + default void saveBatch(Integer roleId, Integer[] menuIds){ + // 先删除 对应的角色菜单 + remove(new LambdaQueryWrapper() + .eq(SysRoleMenu::getRoleId, roleId)); + if (null != menuIds && menuIds.length > 0){ + List list = new ArrayList<>(); + for (Integer menuId : menuIds) { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(roleId); + rm.setMenuId(menuId); + list.add(rm); + } + saveBatch(list); + } + } + + default void deleteRoleMenu(Collection roleIds){ + if (CollectionUtil.isNotEmpty(roleIds)){ + roleIds.forEach(this::removeByRoleId); + } + } + + default void removeByRoleId(Integer roleId){ + remove(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, roleId)); + } + + default boolean checkMenuExistRole(Integer menuId){ + return count(new LambdaQueryWrapper() + .eq(SysRoleMenu::getMenuId, menuId)) > 0; + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleService.java new file mode 100644 index 0000000..018b54e --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysRoleService.java @@ -0,0 +1,78 @@ +package com.chushang.system.service; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.mybatis.enums.Operator; +import com.chushang.system.entity.bo.CancelUserRole; +import com.chushang.system.entity.dto.ListRoleDTO; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.entity.po.SysUser; + +import java.util.List; + +/** + *

+ * 角色用于控制菜单 及 用户的显示 服务类 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface ISysRoleService extends IService { + + List selectRoleAll(SysRole sysRole); + + List selectRolesByUserId(SysUser user); + + AjaxResult selectRoleList(ListRoleDTO role); + + void checkRoleDataScope(SysRole role); + + SysRole selectRoleById(Integer roleId); + + default boolean checkRoleNameUnique(SysRole role){ + int roleId = ObjectUtil.isNull(role.getRoleId()) ? -1 : role.getRoleId(); + SysRole info = getOne(new LambdaQueryWrapper() + .eq(SysRole::getRoleName, role.getRoleName()) + .last(Operator.LIMIT_ONE.getCharacter())); + return ObjectUtil.isNotNull(info) && info.getRoleId() != roleId; + } + + default boolean checkRoleKeyUnique(SysRole role){ + int roleId = ObjectUtil.isNull(role.getRoleId()) ? -1 : role.getRoleId(); + SysRole info = getOne(new LambdaQueryWrapper() + .eq(SysRole::getRoleKey, role.getRoleKey()) + .last(Operator.LIMIT_ONE.getCharacter())); + return ObjectUtil.isNotNull(info) && info.getRoleId() != roleId; + } + + void saveRole(SysRole role); + + default void checkRoleAllowed(SysRole role){ + Integer roleId = role.getRoleId(); + if (StringUtils.isNotNull(roleId) && role.isAdmin()) + { + throw new ResultException("不允许操作超级管理员角色"); + } + } + + void updateRole(SysRole role); + + void authDataScope(SysRole role); + + default void updateRoleStatus(SysRole role){ + updateById(role); + } + + void deleteRoleByIds(Integer[] roleIds); + + void deleteAuthUser(CancelUserRole cancelUserRole); + + void insertAuthUsers(Integer roleId, Integer[] userIds); + + String selectRolesByUserName(String username); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserDataService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserDataService.java new file mode 100644 index 0000000..3e29a2f --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserDataService.java @@ -0,0 +1,61 @@ +package com.chushang.system.service; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.system.entity.bo.DataAuth; +import com.chushang.system.entity.enums.AuthTypeEnum; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.po.SysUserData; + +import java.util.List; + +/** + * @author by zhaowenyuan create 2022/9/14 10:24 + */ +public interface ISysUserDataService extends IService { + + default List listUserData(SysUser sysUser){ + return list(new LambdaQueryWrapper() + .eq(SysUserData::getUserId, sysUser.getUserId())); + } + + default void authData(DataAuth dataAuth){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(SysUserData::getUserId, dataAuth.getUserId()) + .eq(SysUserData::getDataType, dataAuth.getAuthType()); + long count = count(queryWrapper); + // 不存在就进行新增 + if (count <= 0 ){ + save(SysUserData.builder() + .userId(dataAuth.getUserId()) + .dataType(dataAuth.getAuthType()) + .dataValue(dataAuth.getAuthData()) + .build()); + } + // 存在就进行更新 + else { + update(SysUserData.builder() + .dataValue(dataAuth.getAuthData()) + .build(), queryWrapper); + } + } + + + default List getUserAuthData(Integer userId, Integer authType){ + SysUserData userData = getOne(new LambdaQueryWrapper() + .eq(SysUserData::getUserId, userId) + .eq(SysUserData::getDataType, authType)); + if (ObjectUtil.isNotEmpty(userData)){ + return JSONUtil.toList(userData.getDataValue(), Integer.class); + } + return List.of(); + } + + default void authData(List dataAuthList){ + dataAuthList.forEach(this::authData); + } + + void authData(Integer userId, Integer appId, AuthTypeEnum authType); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserPostService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserPostService.java new file mode 100644 index 0000000..92a38ed --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserPostService.java @@ -0,0 +1,33 @@ +package com.chushang.system.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.system.entity.po.SysUserPost; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author by zhaowenyuan create 2022/8/19 11:49 + */ +public interface ISysUserPostService extends IService { + + default void saveOrUpdate(Integer userId, Integer[] roleIdList){ + //先删除用户与岗位 + this.remove(new LambdaQueryWrapper() + .eq(SysUserPost::getUserId, userId)); + + if(roleIdList == null || roleIdList.length == 0){ + return ; + } + + List collect = Arrays.stream(roleIdList).map(s -> { + SysUserPost sysUserRoleEntity = new SysUserPost(); + sysUserRoleEntity.setUserId(userId); + sysUserRoleEntity.setPostId(s); + return sysUserRoleEntity; + }).collect(Collectors.toList()); + this.saveBatch(collect); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserRoleService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserRoleService.java new file mode 100644 index 0000000..1658d0b --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserRoleService.java @@ -0,0 +1,66 @@ +package com.chushang.system.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.system.entity.po.SysUserRole; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + *

+ * 服务类 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface ISysUserRoleService extends IService { + + default void saveOrUpdate(Integer userId, Integer[] roleIdList){ + //先删除用户与角色关系 + this.remove(new LambdaQueryWrapper() + .eq(SysUserRole::getUserId, userId)); + if(roleIdList == null || roleIdList.length == 0){ + return ; + } + this.saveBatch(Arrays.stream(roleIdList).map(s -> { + SysUserRole sysUserRoleEntity = new SysUserRole(); + sysUserRoleEntity.setUserId(userId); + sysUserRoleEntity.setRoleId(s); + return sysUserRoleEntity; + }).collect(Collectors.toList())); + } + + default void saveOrUpdate(Integer[] userIdList, Integer roleId){ + //先删除用户与角色关系 + this.remove(new LambdaQueryWrapper() + .eq(SysUserRole::getRoleId, roleId)); + if(userIdList == null || userIdList.length == 0){ + return ; + } + this.saveBatch(Arrays.stream(userIdList).map(userId -> { + SysUserRole sysUserRoleEntity = new SysUserRole(); + sysUserRoleEntity.setUserId(userId); + sysUserRoleEntity.setRoleId(roleId); + return sysUserRoleEntity; + }).collect(Collectors.toList())); + } + + default void deleteUserRoleByUserId(Integer userId){ + remove(new LambdaQueryWrapper() + .eq(SysUserRole::getUserId, userId)); + } + + default long countUserRoleByRoleId(Integer roleId){ + return count(new LambdaQueryWrapper() + .eq(SysUserRole::getRoleId, roleId)); + } + + default void removeByUserIdAndRoleId(Collection userIds, Integer roleId){ + remove(new LambdaQueryWrapper() + .eq(SysUserRole::getRoleId, roleId) + .in(SysUserRole::getUserId, userIds)); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserService.java new file mode 100644 index 0000000..2d83ff6 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/ISysUserService.java @@ -0,0 +1,80 @@ +package com.chushang.system.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.dto.ListUserDTO; +import com.chushang.system.entity.po.SysUser; + +/** + *

+ * 服务类 + *

+ * + * @author author + * @since 2022-08-18 + */ +public interface ISysUserService extends IService { + + AjaxResult listUser(ListUserDTO listUser); + + SysUser selectUserByUserName(String username); + + SysUser selectUserById(Integer userId); + + void checkUserDataScope(SysUser user); + + void saveUser(SysUser user); + + void update(SysUser user); + + void deleteBatch(Integer[] userIds); + + void checkUserAllowed(SysUser sysUser); + + default boolean updatePassword(Integer userId, String oldPassword, String newPassword){ + return this.update(SysUser.builder() + .password(newPassword) + .updateBy(SecurityUtils.getUsername()) + .build() + ,new LambdaQueryWrapper() + .eq(SysUser::getUserId, userId) + .eq(SysUser::getPassword, oldPassword) + ); + } + + default void updateUserStatus(SysUser user){ + this.update(SysUser.builder() + .status(user.getStatus()) + .updateBy(user.getUpdateBy()) + .build(), new LambdaQueryWrapper() + .eq(SysUser::getUserId, user.getUserId())); + } + + void insertUserAuth(Integer userId, Integer[] roleIds); + + AjaxResult selectAllocatedList(ListUserDTO listUser); + + AjaxResult selectUnallocatedList(ListUserDTO listUser); + + default boolean checkUsername(String username){ + return count(new LambdaQueryWrapper() + .eq(SysUser::getUsername, username)) > 0; + } + + default boolean updateUserProfile(SysUser user){ + return updateById(user); + } + + String selectUserRoleGroup(String username); + + default boolean checkDeptExistUser(Integer deptId){ + return count(new LambdaQueryWrapper() + .eq(SysUser::getDeptId, deptId)) > 0; + } + + void resetPwd(Integer userId, String newPassword); + + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/RemoteService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/RemoteService.java new file mode 100644 index 0000000..75e0aad --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/RemoteService.java @@ -0,0 +1,62 @@ +package com.chushang.system.service; + +import cn.hutool.http.HttpRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 用于 创建 gitlab 账号使用 + */ +@Slf4j +@Service +public class RemoteService { + // 第一步 先飞书 -> 然后 企业微信 -> 然后 bi -> 然后gitlab + + /** + * gitlab 相关: https://docs.gitlab.com/ee/api/users.html#user-creation + * // 注册账号 + * 1. http://192.168.2.50:8090/api/v4/users?private_token=1y41xpK71_QDRkyGGdzq + * + * + * 1. 新增用户 + * 2. 修改用户 --> 需不需要关注修改? + * 3. 删除用户 + * 4. 停用用户 + */ + /** + * git lab 外网ip + */ + private static final String gitLab_host = "http://ds.31games.cn:9000"; + + public void createGitlabUser(){ + HttpRequest request = HttpRequest.post(gitLab_host); + } + + public void delGitlabUser(){ + + } + /** + * ww4ae589e4dd5c9307 --> 武汉叁一的 corpid + * // corpid 与 corpsecret 获取方式 + * corpid 就是企业Id, corpsecret 为应用id, 针对用户的 corpsecret 为通讯录同步 相关, 需要开启其api + * 企业微信相关 : https://developer.work.weixin.qq.com/document/path/95350 + * // 1. 获取 token --> 叁一 微信token 获取 --> 服务器ip 为159.138.134.194 可获取 + * https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ww4ae589e4dd5c9307&corpsecret=eNXi7vze9gESEiTPT834U8d4lg9QmduUq0JC1YrLd3o + * // 2. 创建用户 + * https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=token + * + * 1. 新增账号 + * 2. 修改用户 --> 需不需要关注修改? + * 3. 删除用户 + * 4. 停用用户 + */ + + /** + * // 飞书需要创建 一个应用, 并添加事件以及权限 + * 需要监听飞书的 入职与离职事件 : https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/user/events/created + * + * // + * + */ + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysDictDataService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysDictDataService.java new file mode 100644 index 0000000..a4897df --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysDictDataService.java @@ -0,0 +1,13 @@ +package com.chushang.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.system.entity.po.SysDictData; + +/** + * @author by zhaowenyuan create 2022/8/26 11:42 + */ +public interface SysDictDataService extends IService { + + AjaxResult selectDictDataByType(String dictType); +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysLoginInfoService.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysLoginInfoService.java new file mode 100644 index 0000000..44e1b08 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/SysLoginInfoService.java @@ -0,0 +1,13 @@ +package com.chushang.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.system.entity.po.SysLoginInfo; + +public interface SysLoginInfoService extends IService { + + default AjaxResult saveLoginInfo(SysLoginInfo sysLogininfo){ + return save(sysLogininfo) ? AjaxResult.success() : AjaxResult.error(); + } + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/ISysUserDataServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/ISysUserDataServiceImpl.java new file mode 100644 index 0000000..d13edeb --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/ISysUserDataServiceImpl.java @@ -0,0 +1,55 @@ +package com.chushang.system.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.common.mybatis.enums.Operator; +import com.chushang.system.entity.enums.AuthTypeEnum; +import com.chushang.system.entity.po.SysUserData; +import com.chushang.system.mapper.SysUserDataMapper; +import com.chushang.system.service.ISysUserDataService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author by zhaowenyuan create 2022/9/14 10:24 + */ +@Slf4j +@Service +public class ISysUserDataServiceImpl extends ServiceImpl implements ISysUserDataService { + + /** + * @param userId 当前登录用户 + * @param appId 待授权的应用 + */ + @Override + public void authData(Integer userId, Integer appId, AuthTypeEnum authType) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(SysUserData::getUserId, userId) + .eq(SysUserData::getDataType, authType); + // + SysUserData userData = getOne(queryWrapper + .last(Operator.LIMIT_ONE.getCharacter())); + // 不存在就进行新增 + if (ObjectUtil.isEmpty(userData)){ + save(SysUserData.builder() + .userId(userId) + .dataType(authType) + .dataValue(JSONUtil.toJsonStr(List.of(appId))) + .build()); + } + // 存在就进行更新 + else { + List appIds = + JSONUtil.toList(userData.getDataValue(), Integer.class); + appIds.add(appId); + update(SysUserData.builder() + .dataValue(JSONUtil.toJsonStr(appIds)) + .dataType(authType) + .build(), queryWrapper); + } + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDeptServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..8287045 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,147 @@ +package com.chushang.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.common.core.exception.ServiceException; +import com.chushang.common.core.util.SpringUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.system.annotation.DataScope; +import com.chushang.system.mapper.SysDeptMapper; +import com.chushang.system.service.ISysUserService; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.dto.ListDeptDTO; +import com.chushang.system.entity.po.SysDept; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.entity.vo.TreeSelect; +import com.chushang.system.service.ISysDeptService; +import com.chushang.system.service.ISysRoleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * 部门用于控制各平台的数据权限 服务实现类 + *

+ * @author author + * @since 2022-08-18 + */ +@Slf4j +@Service +public class SysDeptServiceImpl extends ServiceImpl implements ISysDeptService { + + @Autowired + ISysRoleService roleService; + @Autowired + ISysUserService userService; + + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(ListDeptDTO listDept) { + return baseMapper.selectDeptList(listDept); + } + + @Override + public void checkDeptDataScope(Integer deptId) { + if (!SecurityUtils.isAdmin()){ + ListDeptDTO dept = new ListDeptDTO(); + dept.setDeptId(deptId); + List deptList = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (CollectionUtil.isEmpty(deptList)) + { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + @Override + public List buildDeptTreeSelect(List deptList) { + List deptTrees = buildDeptTree(deptList); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + @Override + public List buildDeptTree(List deptList) + { + if (CollectionUtil.isNotEmpty(deptList)){ + List returnList = new ArrayList<>(); + List tempList = + deptList.stream().map(SysDept::getDeptId).collect(Collectors.toList()); + for (SysDept dept : deptList) + { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) + { + recursionFn(deptList, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) + { + returnList = deptList; + } + return returnList; + } + return deptList; + } + + @Override + public List selectDeptListByRoleId(Integer roleId) { + SysRole sysRole = roleService.selectRoleById(roleId); + return baseMapper.selectDeptListByRoleId(roleId, sysRole.isDeptCheckStrictly()); + } + + @Override + public boolean checkDeptExistUser(Integer deptId) { + return userService.checkDeptExistUser(deptId); + } + + @Override + public void updateDeptChildren(List children) { + baseMapper.updateDeptChildren(children); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) + { + List cList = new ArrayList<>(); + for (SysDept n : list) { + if (StringUtils.isNotNull(n.getParentId()) && Objects.equals(n.getParentId(), t.getDeptId())) { + cList.add(n); + } + } + return cList; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) + { + return getChildList(list, t).size() > 0; + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDictDataServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..cbcdb5b --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,51 @@ +package com.chushang.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.system.entity.po.SysDictData; +import com.chushang.system.mapper.SysDictDataMapper; +import com.chushang.system.service.SysDictDataService; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author by zhaowenyuan create 2022/8/26 11:42 + */ +@Slf4j +@Service +public class SysDictDataServiceImpl extends ServiceImpl implements SysDictDataService { + + @Autowired + RedissonClient redissonClient; + + String SYS_DICT_KEY = "sys_dict:"; + + @Override + public AjaxResult selectDictDataByType(String dictType) { + RBucket> bucket = redissonClient.getBucket(getCacheKey(dictType)); + List dictDataList = bucket.get(); + if (CollectionUtil.isNotEmpty(dictDataList)){ + return AjaxResult.success(dictDataList); + } + List list = list(new LambdaQueryWrapper() + .eq(SysDictData::getDictType, dictType) + .eq(SysDictData::getStatus, "0") + .orderByAsc(SysDictData::getDictSort) + ); + if (CollectionUtil.isNotEmpty(list)){ + bucket.set(list); + } + return AjaxResult.success(list); + } + + private String getCacheKey(String cacheKey){ + return SYS_DICT_KEY + cacheKey; + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysLoginInfoServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysLoginInfoServiceImpl.java new file mode 100644 index 0000000..02420f9 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysLoginInfoServiceImpl.java @@ -0,0 +1,14 @@ +package com.chushang.system.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.system.entity.po.SysLoginInfo; +import com.chushang.system.mapper.SysLoginInfoMapper; +import com.chushang.system.service.SysLoginInfoService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class SysLoginInfoServiceImpl extends ServiceImpl implements SysLoginInfoService { + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysMenuServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..95ccbbf --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,356 @@ +package com.chushang.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.common.core.constant.Constants; +import com.chushang.common.core.constant.UserConstants; +import com.chushang.common.core.util.StringUtils; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.entity.dto.ListMenuDTO; +import com.chushang.system.entity.enums.MenuTypeEnum; +import com.chushang.system.entity.po.SysMenu; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.vo.MetaVo; +import com.chushang.system.entity.vo.RouterVo; +import com.chushang.system.entity.vo.TreeSelect; +import com.chushang.system.mapper.SysMenuMapper; +import com.chushang.system.service.ISysMenuService; +import com.chushang.system.service.ISysRoleMenuService; +import com.chushang.system.service.ISysRoleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 服务实现类 + *

+ * + * @author author + * @since 2022-08-18 + */ +@Slf4j +@Service +public class SysMenuServiceImpl extends ServiceImpl implements ISysMenuService { + + @Autowired + ISysRoleService roleService; + @Autowired + ISysRoleMenuService roleMenuService; + + @Override + public Set selectMenuPermsByUserId(Integer userId) { + Set perms = baseMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + if (CollectionUtil.isNotEmpty(perms)){ + perms.stream().filter(StringUtils::isNotEmpty) + .forEach(p->permsSet.addAll(Set.of(p.trim().split(",")))); + } + return permsSet; + } + + @Override + public List selectMenuList(ListMenuDTO menu, Integer userId) { + List menuList; + if (SecurityUtils.isAdmin()) + { + menuList = baseMapper.selectMenuList(menu); + } + else + { + menu.getSqlParam().put("userId", userId); + menuList = baseMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + @Override + public List buildMenuTreeSelect(List menus) { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + @Override + public List selectMenuListByRoleId(Integer roleId) { + SysRole sysRole = roleService.selectRoleById(roleId); + return baseMapper.selectMenuListByRoleId(roleId, sysRole.isMenuCheckStrictly()); + } + + @Override + public boolean checkMenuExistRole(Integer menuId) { + return roleMenuService.checkMenuExistRole(menuId); + } + + @Override + public List selectMenuTreeByUserId(SysUser sysUser) { + List menus; + if (sysUser.isAdmin()) + { + menus = baseMapper.selectMenuTreeAll(); + } + else + { + menus = baseMapper.selectMenuTreeByUserId(sysUser.getUserId()); + } + return getChildPerms(menus, 0); + } + + @Override + public List buildMenus(List menus) { + List routers = new LinkedList<>(); + for (SysMenu menu : menus) + { + RouterVo router = new RouterVo(); + router.setHidden(menu.isVisible()); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.isCache(), menu.getPath())); + List cMenus = menu.getChildren(); + if (!cMenus.isEmpty() && MenuTypeEnum.CATALOG.equals(menu.getMenuType())) + { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } + else if (isMenuFrame(menu)) + { + router.setMeta(null); + List childrenList = new ArrayList<>(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(StringUtils.capitalize(menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.isCache(), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } + // 是否为内链组件 + else if (menu.getParentId() == 0 && isInnerLink(menu)) + { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList<>(); + RouterVo children = new RouterVo(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(StringUtils.capitalize(routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + @Override + public Set selectMenuPermsByRoleId(Integer roleId) { + return baseMapper.selectMenuPermsByRoleId(roleId); + } + + public String getRouteName(SysMenu menu) + { + String routerName = StringUtils.capitalize(menu.getPath()); + // 非外链并且是一级目录(类型为目录) // 此处当为 false + if (isMenuFrame(menu)) + { + routerName = StringUtils.EMPTY; + } + return routerName; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) + { + return menu.getParentId() == 0 && MenuTypeEnum.MENU.equals(menu.getMenuType()) && menu.isFrame(); + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) + { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId() != 0 && isInnerLink(menu)) + { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId() && MenuTypeEnum.CATALOG.equals(menu.getMenuType()) + && menu.isFrame()) + { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) + { + routerPath = "/"; + } + return routerPath; + } + + public static void main(String[] args) { + SysMenuServiceImpl sysMenuService = new SysMenuServiceImpl(); + SysMenu menu = new SysMenu(); + menu.setParentId(0); + menu.setMenuName("系统管理"); + menu.setPath("system"); + menu.setMenuType(MenuTypeEnum.CATALOG); + menu.setFrame(false); + menu.setCache(false); + menu.setVisible(false); + menu.setStatus(false); + String routerPath = sysMenuService.getRouterPath(menu); + System.out.println(routerPath); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) + { + return !menu.isFrame() && StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS); + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) + { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) + { + component = menu.getComponent(); + } + else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId() != 0 && isInnerLink(menu)) + { + component = UserConstants.INNER_LINK; + } + else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) + { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) + { + return menu.getParentId() != 0 && MenuTypeEnum.CATALOG.equals(menu.getMenuType()); + } + + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) + { + List returnList = new ArrayList<>(); + for (SysMenu menu : list) { + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (menu.getParentId() == parentId) { + recursionFn(list, menu); + returnList.add(menu); + } + } + return returnList; + } + + /** + * 构建前端树 + */ + private List buildMenuTree(List menus) { + List returnList = new ArrayList<>(); + + List tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + for (SysMenu menu : menus) { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) + { + returnList = menus; + } + return returnList; + } + /** + * 递归列表 + */ + private void recursionFn(List list, SysMenu t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) + { + List cList = new ArrayList<>(); + for (SysMenu n : list) { + if (Objects.equals(n.getParentId(), t.getMenuId())) { + cList.add(n); + } + } + return cList; + } + + private boolean hasChild(List list, SysMenu t) + { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + */ + public String innerLinkReplaceEach(String path) + { + return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, "." }, + new String[] { "", "", "", "/" }); + } + +} + diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysPermissionServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysPermissionServiceImpl.java new file mode 100644 index 0000000..9f8aa20 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysPermissionServiceImpl.java @@ -0,0 +1,73 @@ +package com.chushang.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.service.ISysMenuService; +import com.chushang.system.service.ISysPermissionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * @author by zhaowenyuan create 2022/8/19 09:43 + */ +@Slf4j +@Service +public class SysPermissionServiceImpl implements ISysPermissionService { + + @Autowired + private ISysMenuService menuService; + + @Override + public Set getRolePermission(SysUser sysUser) { +// // 包含的角色 + List roleList = sysUser.getRoles(); +// if (sysUser.isAdmin()){ +// // 需要 判断下是否包含 特殊管理员 +// +// return Set.of(AuthUtil.SUPER_ADMIN); +// }else { + Set roles = new HashSet<>(); + roleList.stream().filter(Objects::nonNull) + .forEach(r -> roles.addAll(Set.of(r.getRoleKey().trim().split(",")))); + return roles; +// } + } + + /** + * 获取菜单数据权限 + */ + @Override + public Set getMenuPermission(SysUser sysUser) { + Set perms = new HashSet<>(); + List roles = sysUser.getRoles(); + // 包含有 admin 角色 --> 即为超级管理员账号 + if (sysUser.isAdmin()){ + perms.add("*:*:*"); + }else { + // 判断 是否包含有管理员角色 + // roles 必定不为空 + if (CollectionUtil.isNotEmpty(roles)){ + for (SysRole role : roles) { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + // 添加 roleIds + sysUser.setRoleIds(roles.stream().map(SysRole::getRoleId).toArray(Integer[]::new)); + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(sysUser.getUserId())); + } + } + return perms; + } + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleDeptServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleDeptServiceImpl.java new file mode 100644 index 0000000..e8a6b07 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleDeptServiceImpl.java @@ -0,0 +1,22 @@ +package com.chushang.system.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.system.entity.po.SysRoleDept; +import com.chushang.system.mapper.SysRoleDeptMapper; +import com.chushang.system.service.ISysRoleDeptService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author author + * @since 2022-08-18 + */ +@Slf4j +@Service +public class SysRoleDeptServiceImpl extends ServiceImpl implements ISysRoleDeptService { + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleMenuServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleMenuServiceImpl.java new file mode 100644 index 0000000..3bd742c --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleMenuServiceImpl.java @@ -0,0 +1,22 @@ +package com.chushang.system.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.system.entity.po.SysRoleMenu; +import com.chushang.system.mapper.SysRoleMenuMapper; +import com.chushang.system.service.ISysRoleMenuService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author author + * @since 2022-08-18 + */ +@Slf4j +@Service +public class SysRoleMenuServiceImpl extends ServiceImpl implements ISysRoleMenuService { + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..e863ceb --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,180 @@ +package com.chushang.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.common.core.exception.ServiceException; +import com.chushang.common.core.util.SpringUtils; +import com.chushang.common.core.util.StringUtils; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.mybatis.utils.PageUtils; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.annotation.DataScope; +import com.chushang.system.mapper.SysRoleMapper; +import com.chushang.system.service.ISysRoleMenuService; +import com.chushang.system.entity.bo.CancelUserRole; +import com.chushang.system.entity.dto.ListRoleDTO; +import com.chushang.system.entity.po.SysRole; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.vo.LoginUser; +import com.chushang.system.service.ISysRoleDeptService; +import com.chushang.system.service.ISysRoleService; +import com.chushang.system.service.ISysUserRoleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + *

+ * 角色用于控制菜单 及 用户的显示 服务实现类 + *

+ * + * @author author + * @since 2022-08-18 + */ +@Slf4j +@Service +public class SysRoleServiceImpl extends ServiceImpl implements ISysRoleService { + + @Autowired + ISysRoleMenuService roleMenuService; + @Autowired + ISysRoleDeptService roleDeptService; + @Autowired + ISysUserRoleService userRoleService; + + @Override + @DataScope(deptAlias = "d") + public List selectRoleAll(SysRole sysRole) { + return baseMapper.selectRoleList(sysRole); + } + + @Override + public List selectRolesByUserId(SysUser user) { + // 当前用户的角色 + List userRoles = user.getRoles(); + // 当前登录用户的所有角色 --> + List roles = SpringUtils.getAopProxy(this).selectRoleAll(new SysRole()); + for (SysRole role : roles) { + for (SysRole userRole : userRoles) { + if (Objects.equals(role.getRoleId(), userRole.getRoleId())) { + role.setFlag(true); + break; + } + } + } + return roles; + } + + @Override + @DataScope(deptAlias = "d") + public AjaxResult selectRoleList(ListRoleDTO listRole) { + Page page = new Page<>(listRole.getPage(), listRole.getLimit()); + List listAfDataVOList = baseMapper.listRole(listRole, page); + + return AjaxResult.success(new PageUtils( + listAfDataVOList, + page.getTotal(), + page.getPages(), + page.getCurrent())); + } + + @Override + @DataScope(deptAlias = "d") + public void checkRoleDataScope(SysRole role) { + // 登录用户非管理员时进行判断 , 判断当前登录用户的角色是否包含以下角色 + if (!SecurityUtils.isAdmin()) { + List roles = baseMapper.selectRoleList(role); + if (CollectionUtil.isEmpty(roles)) { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + + @Override + public SysRole selectRoleById(Integer roleId) { + return baseMapper.selectRoleById(roleId); + } + + @Override + @Transactional + public void saveRole(SysRole role) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser sysUser = loginUser.getSysUser(); + Integer deptId = sysUser.getDeptId(); + + role.setDeptId(deptId); + save(role); + + roleMenuService.saveBatch(role.getRoleId(), role.getMenuIds()); + } + + @Override + @Transactional + public void updateRole(SysRole role) { + updateById(role); + + roleMenuService.saveBatch(role.getRoleId(), role.getMenuIds()); + } + + @Override + @Transactional + public void authDataScope(SysRole role) { + // 修改角色信息 + updateById(role); + // 删除角色与部门关联 + roleDeptService.saveBatch(role.getRoleId(), role.getDeptIds()); + } + + @Override + @Transactional + public void deleteRoleByIds(Integer[] roleIds) { + for (Integer roleId : roleIds) { + SysRole sysRole = new SysRole(roleId); + if (2 == roleId){ + continue; + } + checkRoleAllowed(sysRole); + + SpringUtils.getAopProxy(this).checkRoleDataScope(sysRole); + + SysRole role = selectRoleById(roleId); + + if (userRoleService.countUserRoleByRoleId(roleId) > 0) { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuService.deleteRoleMenu(Arrays.asList(roleIds)); + // 删除角色与部门关联 + roleDeptService.deleteRoleDept(Arrays.asList(roleIds)); + // 删除角色 + baseMapper.deleteBatchIds(Arrays.asList(roleIds)); + } + + @Override + public void deleteAuthUser(CancelUserRole cancelUserRole) { + userRoleService.removeByUserIdAndRoleId( + Set.of(cancelUserRole.getUserIds()), + cancelUserRole.getRoleId()); + } + + @Override + public void insertAuthUsers(Integer roleId, Integer[] userIds) { + userRoleService.saveOrUpdate(userIds, roleId); + } + + @Override + public String selectRolesByUserName(String username) { + Set roleList = baseMapper.selectRoleByUsername(username); + return CollectionUtil.isEmpty(roleList) ? StringUtils.EMPTY : roleList.stream().map(SysRole::getRoleName) + .collect(Collectors.joining(",")); + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserRoleServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserRoleServiceImpl.java new file mode 100644 index 0000000..836379f --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserRoleServiceImpl.java @@ -0,0 +1,22 @@ +package com.chushang.system.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.system.entity.po.SysUserRole; +import com.chushang.system.mapper.SysUserRoleMapper; +import com.chushang.system.service.ISysUserRoleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author author + * @since 2022-08-18 + */ +@Slf4j +@Service +public class SysUserRoleServiceImpl extends ServiceImpl implements ISysUserRoleService { + +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserServiceImpl.java b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..88043ca --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/java/com/chushang/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,227 @@ +package com.chushang.system.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.chushang.common.core.exception.ResultException; +import com.chushang.common.core.exception.ServiceException; +import com.chushang.common.core.util.IdUtils; +import com.chushang.common.core.util.SpringUtils; +import com.chushang.common.core.web.AjaxResult; +import com.chushang.common.mybatis.base.BaseEntity; +import com.chushang.common.mybatis.utils.PageUtils; +import com.chushang.security.service.TokenService; +import com.chushang.security.utils.SecurityUtils; +import com.chushang.system.annotation.DataScope; +import com.chushang.system.mapper.SysUserMapper; +import com.chushang.system.service.ISysUserService; +import com.chushang.system.entity.dto.ListUserDTO; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.po.SysUserRole; +import com.chushang.system.service.ISysMenuService; +import com.chushang.system.service.ISysRoleService; +import com.chushang.system.service.ISysUserRoleService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +/** + *

+ * 服务实现类 + *

+ * + * @author author + * @since 2022-08-18 + */ +@Slf4j +@Service +public class SysUserServiceImpl extends ServiceImpl implements ISysUserService { + + @Autowired + ISysRoleService sysRoleService; + @Autowired + ISysMenuService sysMenuService; + @Autowired + ISysUserRoleService userRoleService; + @Autowired + TokenService tokenService; + + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public AjaxResult listUser(ListUserDTO listUser) { + Page page = new Page<>(listUser.getPage(),listUser.getLimit()); + + List listAfDataVOList = baseMapper.listUser(listUser, + page); + return AjaxResult.success(new PageUtils( + listAfDataVOList, + page.getTotal(), + page.getPages(), + page.getCurrent())); + } + + @Override + public SysUser selectUserByUserName(String username) { + return this.baseMapper.selectUserByUserName(username); + } + + @Override + public SysUser selectUserById(Integer userId) { + SysUser sysUser = baseMapper.selectUserById(userId); + if (ObjectUtil.isNotEmpty(sysUser)){ + sysUser.setSalt(""); + sysUser.setPassword(""); + } + return sysUser; + } + + /** + * 判断当前登录用户 有没有 被修改用户的权限 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public void checkUserDataScope(SysUser user) + { + //登录用户非管理员时进行判断 && 待操作的用户也不为管理员 + if (!SecurityUtils.isAdmin()) + { + List users = baseMapper.selectUserList(user); + if (ObjectUtil.isEmpty(users)) + { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + @Override + @Transactional + public void saveUser(SysUser user) { + + SpringUtils.getAopProxy(this).checkUsernameUnique(user.getUsername()); + user.setCreateTime(LocalDateTime.now()); + String salt = IdUtils.getId(10); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword(), salt)); + user.setSalt(salt); + this.save(user); + + //保存用户与角色关系 + userRoleService.saveOrUpdate(user.getUserId(), user.getRoleIds()); + + } + + @Override + public void update(SysUser user) { + if(StringUtils.isBlank(user.getPassword())){ + user.setPassword(null); + } + this.updateById(user); + + //保存用户与角色关系 + userRoleService.saveOrUpdate(user.getUserId(), user.getRoleIds()); + + } + + @Override + @Transactional + public void deleteBatch(Integer[] userIds) { + + for (Integer userId : userIds) + { + SysUser sysUser = SysUser.builder().userId(userId).build(); + SpringUtils.getAopProxy(this).checkUserAllowed(sysUser); + SpringUtils.getAopProxy(this).checkUserDataScope(sysUser); + } + + this.removeByIds(Arrays.asList(userIds)); + + //先删除用户与角色关系 + userRoleService.remove(new LambdaQueryWrapper() + .in(SysUserRole::getUserId, Arrays.asList(userIds))); + + } + + /** + * 校验用户是否允许操作 + */ + @Override + public void checkUserAllowed(SysUser sysUser) { + if (ObjectUtil.isNotNull(sysUser.getUserId()) && sysUser.isAdmin()){ + throw new ResultException("不允许操作超级管理员用户"); + } + } + + @Override + @Transactional + public void insertUserAuth(Integer userId, Integer[] roleIds) { + userRoleService.saveOrUpdate(userId, roleIds); + } + + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public AjaxResult selectAllocatedList(ListUserDTO listUser) { + Page page = new Page<>(listUser.getPage(),listUser.getLimit()); + + List userList = baseMapper.selectAllocatedList(listUser, + page); + return AjaxResult.success(new PageUtils( + userList, + page.getTotal(), + page.getPages(), + page.getCurrent())); + } + + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public AjaxResult selectUnallocatedList(ListUserDTO listUser) { + Page page = new Page<>(listUser.getPage(),listUser.getLimit()); + + List userList = baseMapper.selectUnallocatedList(listUser, + page); + return AjaxResult.success(new PageUtils( + userList, + page.getTotal(), + page.getPages(), + page.getCurrent())); + } + + @Override + public String selectUserRoleGroup(String username) { + return sysRoleService.selectRolesByUserName(username); + } + + @Override + public void resetPwd(Integer userId, String newPassword) { + SysUser user = getOne(new LambdaQueryWrapper() + .eq(SysUser::getUserId, userId)); + if (ObjectUtil.isEmpty(user)){ + throw new ResultException("用户不存在"); + } + String salt = IdUtils.getId(10); + //sha256加密 + newPassword = SecurityUtils.encryptPassword(newPassword, salt); + updateById(SysUser.builder() + .userId(userId) + .password(newPassword) + .salt(salt) + .updateBy(SecurityUtils.getUsername()) + .build()); + // 强退用户 + tokenService.forcedRetreat(userId); + } + + + private void checkUsernameUnique(String username) { + long count = count(new LambdaQueryWrapper() + .eq(SysUser::getUsername, username)); + if (count > 0){ + throw new ResultException("保存用户'" + username + "'失败,注册账号已存在"); + } + } +} diff --git a/chushang-modules/chushang-module-system/system-service/src/main/resources/application.yml b/chushang-modules/chushang-module-system/system-service/src/main/resources/application.yml new file mode 100644 index 0000000..f7ebb5d --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/resources/application.yml @@ -0,0 +1,70 @@ +server: + #开启优雅停机 + shutdown: graceful + port: 8085 + servlet: + context-path: /cs/system + tomcat: + uri-encoding: UTF-8 + threads: + max: 200 + min-spare: 25 +spring: + application: + name: @artifactId@ + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + username: ${conf.jdbc.cacc.master.username} + password: ${conf.jdbc.cacc.master.password} + url: jdbc:mysql://${conf.jdbc.cacc.master.host}:${conf.jdbc.cacc.master.port}/${conf.jdbc.cacc.master.database}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai + devtools: + restart: + enabled: true + jackson: + default-property-inclusion: ALWAYS + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss +# mybaits-plus配置 +mybatis-plus: + mapper-locations: classpath:/mapper/**/*.xml + global-config: + db-config: + id-type: auto + table-underline: true + logic-delete-value: 1 + logic-not-delete-value: 0 + configuration: + map-underscore-to-camel-case: true + cache-enabled: false + call-setters-on-nulls: true + jdbc-type-for-null: 'null' + default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +# feign 配置 +feign: + sentinel: + enabled: true + okhttp: + enabled: true + httpclient: + enabled: false + client: + config: + default: + connectTimeout: 10000 + readTimeout: 10000 + compression: + request: + enabled: true + response: + enabled: true +# 暴露监控端点 +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-service/src/main/resources/bootstrap.yml b/chushang-modules/chushang-module-system/system-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..7662366 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/resources/bootstrap.yml @@ -0,0 +1,40 @@ +server: + tomcat: + uri-encoding: UTF-8 + threads: + max: 200 + min-spare: 25 + connection-timeout: 5000ms +spring: + cloud: + loadbalancer: + cache: + enabled: false + nacos: + server-addr: ${nacos.host} + username: nacos + password: nacos + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + group: ${nacos.group} + service: ${spring.application.name} + config: + server-addr: ${spring.cloud.nacos.discovery.server-addr} + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + file-extension: yml + refresh-enabled: true + prefix: ${spring.application.name} + shared-configs: + - dataId: application-common.${spring.cloud.nacos.config.file-extension} + group: ${nacos.group} + refresh: ${spring.cloud.nacos.config.refresh-enabled} + extension-configs: + - data-id: ${spring.application.name}.${spring.cloud.nacos.config.file-extension} + group: ${spring.cloud.nacos.discovery.group} + refresh: ${spring.cloud.nacos.config.refresh-enabled} + profiles: + active: @profiles.active@ + main: + allow-bean-definition-overriding: true \ No newline at end of file diff --git a/chushang-modules/chushang-module-system/system-service/src/main/resources/logback-nacos.xml b/chushang-modules/chushang-module-system/system-service/src/main/resources/logback-nacos.xml new file mode 100644 index 0000000..ac72426 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/resources/logback-nacos.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + %boldCyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) TRACE[%X{trace:-}/%X{span:-}] [%highlight(%-5p)][%cyan(%t)][%magenta(%c{50})-%boldMagenta(%M)][%cn] - %msg%n + + UTF-8 + + + + + + ${log.path}/info.log + + ${log.path}/info.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + TRACE[%X{trace:-}/%X{span:-}] %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + info + + + + + ${log.path}/debug.log + + ${log.path}/debug.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + TRACE[%X{trace:-}/%X{span:-}] %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + debug + + + + + ${log.path}/error.log + + ${log.path}/error.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + TRACE[%X{trace:-}/%X{span:-}] %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysDeptMapper.xml b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysDeptMapper.xml new file mode 100644 index 0000000..6becad1 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysDeptMapper.xml @@ -0,0 +1,65 @@ + + + + + select d.dept_id, d.parent_dept_id, d.ancestors, d.dept_name, d.order_num, d.status, d.del_state, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + diff --git a/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysMenuMapper.xml b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysMenuMapper.xml new file mode 100644 index 0000000..6669cd9 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysMenuMapper.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, + menu_name, + parent_id, + order_num, + path, + component, + `query`, + frame, + cache, + menu_type, + visible, + status, + ifnull(perms, '') as perms, + icon, + create_time, + del_state, + create_by, + update_time, + update_by + from sys_menu + where del_state = FALSE + + + + + + + + + + + + + diff --git a/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysRoleMapper.xml b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysRoleMapper.xml new file mode 100644 index 0000000..393d633 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysRoleMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.order_num, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_state, r.create_time, r.remark, r.update_time, r.create_by + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + diff --git a/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysUserMapper.xml b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysUserMapper.xml new file mode 100644 index 0000000..b2b6b26 --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/main/resources/mapper/SysUserMapper.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.username, u.password, u.status, u.del_state, u.create_by, u.create_time,u.update_time,u.salt, + d.parent_dept_id, d.ancestors, d.dept_name, d.order_num as dept_order_num,d.status as dept_status, + r.role_id, r.role_name, r.role_key,r.order_num as role_order_num, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + diff --git a/chushang-modules/chushang-module-system/system-service/src/test/java/DemoTest.java b/chushang-modules/chushang-module-system/system-service/src/test/java/DemoTest.java new file mode 100644 index 0000000..9dc21fc --- /dev/null +++ b/chushang-modules/chushang-module-system/system-service/src/test/java/DemoTest.java @@ -0,0 +1,100 @@ +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import com.chushang.common.core.util.StringUtils; +import com.chushang.security.service.TokenService; +import com.chushang.system.SystemApplication; +import com.chushang.system.entity.enums.AuthTypeEnum; +import com.chushang.system.entity.po.SysMenu; +import com.chushang.system.entity.po.SysUser; +import com.chushang.system.entity.po.SysUserData; +import com.chushang.system.entity.vo.LoginUser; +import com.chushang.system.entity.vo.RouterVo; +import com.chushang.system.service.*; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.*; + +/** + * @author by zhaowenyuan create 2022/8/22 15:20 + */ +@Slf4j +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SystemApplication.class) +public class DemoTest { + + @Autowired + ISysMenuService menuService; + @Autowired + ISysDeptService sysDeptService; + @Autowired + ISysUserService sysUserService; + @Autowired + ISysRoleService sysRoleService; + @Autowired + ISysPermissionService permissionService; + @Autowired + ISysUserDataService userDataService; + @Autowired + TokenService tokenService; + + @Test + public void test(){ + String username = "zhaowenyuan@31gamestudio.com"; +// String username = "admin"; + + SysUser sysUser = sysUserService.selectUserByUserName(username); + if (StringUtils.isNull(sysUser)) { + log.error("密码错误"); + } + + // 角色集合 + Set roles = permissionService.getRolePermission(sysUser); + // 权限集合 + Set permissions = permissionService.getMenuPermission(sysUser); + + LoginUser sysUserVo = new LoginUser(); + sysUserVo.setUserId(sysUser.getUserId()); + sysUserVo.setUsername(sysUser.getUsername()); + sysUserVo.setSysUser(sysUser); + sysUserVo.setRoles(roles); + sysUserVo.setPermissions(permissions); + + List userDataList = userDataService.listUserData(sysUser); + // 默认传递一个空Map + sysUserVo.setAuthDataMap(new HashMap<>()); + if (CollectionUtil.isNotEmpty(userDataList)){ + Map> authDataMap = new HashMap<>(); + // 对应的数据权限 + for (SysUserData sysUserData : userDataList) { + authDataMap.put(sysUserData.getDataType().getCodeType(), + new HashSet<>(JSONUtil.toList(sysUserData.getDataValue(), String.class))); + } + sysUserVo.setAuthDataMap(authDataMap); + } + + log.info("{}", JSONUtil.toJsonStr(sysUserVo)); + } + + @Test + public void authTest(){ + // + userDataService.authData(999, 998, AuthTypeEnum.APP); + } + + @Test + public void menuTest(){ +// List menus = menuService.list( +// new LambdaQueryWrapper() +// .eq(SysMenu::getMenuId, 2136) +// ); + List menus = menuService.selectMenuTreeByUserId(new SysUser(1)); + List routerVos = menuService.buildMenus(menus); + log.info("{}", JSONUtil.toJsonStr(routerVos)); + } + +} diff --git a/chushang-modules/chushang-module-system/system.md b/chushang-modules/chushang-module-system/system.md new file mode 100644 index 0000000..36bd31a --- /dev/null +++ b/chushang-modules/chushang-module-system/system.md @@ -0,0 +1,17 @@ +管理系统部分设计文档 + +菜单 | 数据 -> 最底层 + 菜单可被赋予角色, 数据可被赋予部门 +角色 | 部门 -> 角色控制菜单, 系统数据的显示。部门控制对应的数据权限显示 + 角色可被赋予用户, 部门可被赋予用户 +用户 + +1. 菜单管理 -> 菜单管理分为目录->菜单->按钮等, 不参与权限分配, 仅仅用作显示 +2. 角色管理 -> 角色管理 -> 主要进行创建角色, 主要用于管理页面 以及 按钮权限操作等, 角色的权限分为 4点, 自定义, 本级, 本级及以下, 全部 等, 以最高级别管理员为主, + +3. 部门管理 -> 部门管理 -> 主要进行部门创建, 用于用户所属部门, 不同部门页面 +4. 数据管理 -> 数据管理 -> 整体管理不同平台数据的展示操作等, 针对每项数据都分为 增删改查, 仅仅是页面内部的, + +5. 用户管理 -> 用户管理 -> 主要为登录账号, 账号可分配角色以及数据, 用户单独分配角色以及数据 + +6. 授权管理 -> 单独服务 用于授权。 \ No newline at end of file diff --git a/chushang-modules/pom.xml b/chushang-modules/pom.xml new file mode 100644 index 0000000..266782d --- /dev/null +++ b/chushang-modules/pom.xml @@ -0,0 +1,21 @@ + + + + chushangcloud + com.chushang + 1.0.0 + + 4.0.0 + + chushang-modules + pom + + chushang-module-auth + chushang-module-gateway + chushang-module-system + chushang-module-oss + + + \ No newline at end of file diff --git a/chushang-visual/chushang-admin/.gitignore b/chushang-visual/chushang-admin/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-visual/chushang-admin/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-visual/chushang-admin/pom.xml b/chushang-visual/chushang-admin/pom.xml new file mode 100644 index 0000000..bd3f2dc --- /dev/null +++ b/chushang-visual/chushang-admin/pom.xml @@ -0,0 +1,68 @@ + + + + chushang-visual + com.chushang + 1.0.0 + + 4.0.0 + + chushang-admin + 叁一监控服务 + + + 17 + 17 + + + + + com.chushang + chushang-common-core + 1.0.0 + + + de.codecentric + spring-boot-admin-starter-server + 2.6.3 + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-mail + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/chushang-visual/chushang-admin/src/main/java/com/chushang/admin/SanyiAdminApplication.java b/chushang-visual/chushang-admin/src/main/java/com/chushang/admin/SanyiAdminApplication.java new file mode 100644 index 0000000..363514e --- /dev/null +++ b/chushang-visual/chushang-admin/src/main/java/com/chushang/admin/SanyiAdminApplication.java @@ -0,0 +1,18 @@ +package com.chushang.admin; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @author by zhaowenyuan create 2022/8/9 15:22 + */ +@EnableAdminServer +@EnableDiscoveryClient +@SpringBootApplication +public class SanyiAdminApplication { + public static void main(String[] args) { + SpringApplication.run(SanyiAdminApplication.class,args); + } +} diff --git a/chushang-visual/chushang-admin/src/main/java/com/chushang/admin/config/SecuritySecureConfig.java b/chushang-visual/chushang-admin/src/main/java/com/chushang/admin/config/SecuritySecureConfig.java new file mode 100644 index 0000000..663cfbf --- /dev/null +++ b/chushang-visual/chushang-admin/src/main/java/com/chushang/admin/config/SecuritySecureConfig.java @@ -0,0 +1,61 @@ +package com.chushang.admin.config; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import java.util.UUID; + +@Configuration(proxyBeanMethods = false) +public class SecuritySecureConfig extends WebSecurityConfigurerAdapter { + + private final AdminServerProperties adminServer; + + private final SecurityProperties security; + + public SecuritySecureConfig(AdminServerProperties adminServer, SecurityProperties security) { + this.adminServer = adminServer; + this.security = security; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(this.adminServer.path("/")); + + http.authorizeRequests( + (authorizeRequests) -> authorizeRequests.antMatchers(this.adminServer.path("/assets/**")).permitAll() + .antMatchers(this.adminServer.path("/actuator/info")).permitAll() + .antMatchers(this.adminServer.path("/actuator/health")).permitAll() + .antMatchers(this.adminServer.path("/login")).permitAll().anyRequest().authenticated() + ).formLogin( + (formLogin) -> formLogin.loginPage(this.adminServer.path("/login")).successHandler(successHandler).and() + ).logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout"))).httpBasic(Customizer.withDefaults()) + .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .ignoringRequestMatchers( + new AntPathRequestMatcher(this.adminServer.path("/instances"), + HttpMethod.POST.toString()), + new AntPathRequestMatcher(this.adminServer.path("/instances/*"), + HttpMethod.DELETE.toString()), + new AntPathRequestMatcher(this.adminServer.path("/actuator/**")) + )) + .rememberMe((rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600)); + } + + // Required to provide UserDetailsService for "remember functionality" + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser(security.getUser().getName()) + .password("{noop}" + security.getUser().getPassword()).roles("USER"); + } + +} \ No newline at end of file diff --git a/chushang-visual/chushang-admin/src/main/resources/bootstrap.yml b/chushang-visual/chushang-admin/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..0a7de98 --- /dev/null +++ b/chushang-visual/chushang-admin/src/main/resources/bootstrap.yml @@ -0,0 +1,38 @@ +spring: + application: + name: @artifactId@ + cloud: + nacos: + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + group: ${nacos.group} + metadata: + user: + name: sanyi + password: sanyiAdmin@.1 + management: + context-path: /actuator + config: + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + server-addr: ${spring.cloud.nacos.discovery.server-addr} + file-extension: yml + shared-configs: + - dataId: application-admin-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + group: COMMON_GROUP + - dataId: application-mail-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + group: COMMON_GROUP + - dataId: application-log-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + group: COMMON_GROUP + refresh: true + thymeleaf: + check-template: false + check-template-location: false + profiles: + active: @profiles.active@ + main: + allow-bean-definition-overriding: true + + + diff --git a/chushang-visual/chushang-admin/src/main/resources/logback-nacos.xml b/chushang-visual/chushang-admin/src/main/resources/logback-nacos.xml new file mode 100644 index 0000000..bbbfc1b --- /dev/null +++ b/chushang-visual/chushang-admin/src/main/resources/logback-nacos.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + UTF-8 + + + + + ${log.path}/info.log + + ${log.path}/info.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + info + + + + + ${log.path}/debug.log + + ${log.path}/debug.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + debug + + + + + ${log.path}/error.log + + ${log.path}/error.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chushang-visual/chushang-sentinel/.gitignore b/chushang-visual/chushang-sentinel/.gitignore new file mode 100644 index 0000000..8c1ecef --- /dev/null +++ b/chushang-visual/chushang-sentinel/.gitignore @@ -0,0 +1,64 @@ +### gradle ### +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea/* +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log +*.log.gz + +### xxl-job log ### +/xxl-job/ + + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata +.fastRequest diff --git a/chushang-visual/chushang-sentinel/pom.xml b/chushang-visual/chushang-sentinel/pom.xml new file mode 100644 index 0000000..ea5625c --- /dev/null +++ b/chushang-visual/chushang-sentinel/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + + com.chushang + chushang-visual + 1.0.0 + + + chushang-sentinel + jar + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-web-servlet + + + com.alibaba.csp + sentinel-transport-simple-http + + + com.alibaba.csp + sentinel-parameter-flow-control + + + com.alibaba.csp + sentinel-api-gateway-adapter-common + + + org.springframework.boot + spring-boot-starter-web + + + commons-lang + commons-lang + 2.6 + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + org.apache.httpcomponents + httpasyncclient + + + org.apache.httpcomponents + httpcore-nio + + + com.alibaba.csp + sentinel-datasource-nacos + + + org.springframework.boot + spring-boot-configuration-processor + + + com.chushang + chushang-common-mongo + + + + + sanyi-sentinel + + + src/main/resources + true + + + src/main/webapp/ + + resources/node_modules/** + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/SanyiSentinelApplication.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/SanyiSentinelApplication.java new file mode 100644 index 0000000..9b247b2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/SanyiSentinelApplication.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard; + +import com.alibaba.csp.sentinel.init.InitExecutor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sentinel dashboard application. + * + * @author Carpenter Lee + */ +@SpringBootApplication +public class SanyiSentinelApplication { + + public static void main(String[] args) { + triggerSentinelInit(); + SpringApplication.run(SanyiSentinelApplication.class, args); + } + + private static void triggerSentinelInit() { + new Thread(InitExecutor::doInit).start(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthAction.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthAction.java new file mode 100644 index 0000000..0976bf1 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthAction.java @@ -0,0 +1,44 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.auth; + +import java.lang.annotation.*; + +/** + * @author lkxiaolou + * @since 1.7.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Target({ ElementType.METHOD }) +public @interface AuthAction { + + /** + * @return the privilege type + */ + AuthService.PrivilegeType value(); + + /** + * @return the target name to control + */ + String targetName() default "app"; + + /** + * @return the message when permission is denied + */ + String message() default "Permission denied"; + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthService.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthService.java new file mode 100644 index 0000000..2c60d8f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthService.java @@ -0,0 +1,112 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.auth; + +/** + * Interface for authentication and authorization. + * + * @author Carpenter Lee + * @since 1.5.0 + */ +public interface AuthService { + + /** + * Get the authentication user. + * @param request the request contains the user information + * @return the auth user represent the current user, when the user is illegal, a null + * value will return. + */ + AuthUser getAuthUser(R request); + + /** + * Privilege type. + */ + enum PrivilegeType { + + /** + * Read rule + */ + READ_RULE, + /** + * Create or modify rule + */ + WRITE_RULE, + /** + * Delete rule + */ + DELETE_RULE, + /** + * Read metrics + */ + READ_METRIC, + /** + * Add machine + */ + ADD_MACHINE, + /** + * All privileges above are granted. + */ + ALL + + } + + /** + * Represents the current user. + */ + interface AuthUser { + + /** + * Query whether current user has the specific privilege to the target, the target + * may be an app name or an ip address, or other destination. + *

+ * This method will use return value to represent whether user has the specific + * privileges to the target, but to throw a RuntimeException to represent no auth + * is also a good way. + *

+ * @param target the target to check + * @param privilegeType the privilege type to check + * @return if current user has the specific privileges to the target, return true, + * otherwise return false. + */ + boolean authTarget(String target, PrivilegeType privilegeType); + + /** + * Check whether current user is a super-user. + * @return if current user is super user return true, else return false. + */ + boolean isSuperUser(); + + /** + * Get current user's nick name. + * @return current user's nick name. + */ + String getNickName(); + + /** + * Get current user's login name. + * @return current user's login name. + */ + String getLoginName(); + + /** + * Get current user's ID. + * @return ID of current user + */ + String getId(); + + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthorizationInterceptor.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthorizationInterceptor.java new file mode 100644 index 0000000..816f9d8 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthorizationInterceptor.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.auth; + +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.fastjson.JSON; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.Method; + +/** + * The web interceptor for privilege-based authorization. + * + * @author lkxiaolou + * @since 1.7.1 + */ +@Component +public class AuthorizationInterceptor implements HandlerInterceptor { + + @Autowired + private AuthService authService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + if (handler.getClass().isAssignableFrom(HandlerMethod.class)) { + Method method = ((HandlerMethod) handler).getMethod(); + + AuthAction authAction = method.getAnnotation(AuthAction.class); + if (authAction != null) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + if (authUser == null) { + responseNoPrivilegeMsg(response, authAction.message()); + return false; + } + String target = request.getParameter(authAction.targetName()); + + if (!authUser.authTarget(target, authAction.value())) { + responseNoPrivilegeMsg(response, authAction.message()); + return false; + } + } + } + + return true; + } + + private void responseNoPrivilegeMsg(HttpServletResponse response, String message) throws IOException { + Result result = Result.ofFail(-1, message); + response.addHeader("Content-Type", "application/json;charset=UTF-8"); + response.getOutputStream().write(JSON.toJSONBytes(result)); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/FakeAuthServiceImpl.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/FakeAuthServiceImpl.java new file mode 100644 index 0000000..979ee56 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/FakeAuthServiceImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.auth; + +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; + +/** + * A fake AuthService implementation, which will pass all user auth checking. + * + * @author Carpenter Lee + * @since 1.5.0 + */ +@Component +public class FakeAuthServiceImpl implements AuthService { + + @Override + public AuthUser getAuthUser(HttpServletRequest request) { + return new AuthUserImpl(); + } + + static final class AuthUserImpl implements AuthUser { + + @Override + public boolean authTarget(String target, PrivilegeType privilegeType) { + // fake implementation, always return true + return true; + } + + @Override + public boolean isSuperUser() { + // fake implementation, always return true + return true; + } + + @Override + public String getNickName() { + return "FAKE_NICK_NAME"; + } + + @Override + public String getLoginName() { + return "FAKE_LOGIN_NAME"; + } + + @Override + public String getId() { + return "FAKE_EMP_ID"; + } + + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/LoginAuthenticationFilter.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/LoginAuthenticationFilter.java new file mode 100644 index 0000000..1b63873 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/LoginAuthenticationFilter.java @@ -0,0 +1,133 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.auth; + +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + *

+ * The Servlet filter for authentication. + *

+ * + *

+ * Note: some urls are excluded as they needn't auth, such as: + *

+ *
    + *
  • index url: {@code /}
  • + *
  • authentication request url: {@code /login}, {@code /logout}
  • + *
  • machine registry: {@code /registry/machine}
  • + *
  • static resources
  • + *
+ * + * The excluded urls and urlSuffixes could be configured in {@code application.properties} + * file. + * + * @author cdfive + * @since 1.6.0 + */ +@Component +public class LoginAuthenticationFilter implements Filter { + + private static final String URL_SUFFIX_DOT = "."; + + private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); + + /** + * Some urls which needn't auth, such as /auth/login, /registry/machine and so on. + */ + @Value("#{'${auth.filter.exclude-urls}'.split(',')}") + private List authFilterExcludeUrls; + + /** + * Some urls with suffixes which needn't auth, such as htm, html, js and so on. + */ + @Value("#{'${auth.filter.exclude-url-suffixes}'.split(',')}") + private List authFilterExcludeUrlSuffixes; + + /** + * Authentication using AuthService interface. + */ + @Autowired + private AuthService authService; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + + String servletPath = httpRequest.getServletPath(); + + // Exclude the urls which needn't auth + if (authFilterExcludeUrls.stream().anyMatch(s -> PATH_MATCHER.match(s, servletPath))) { + chain.doFilter(request, response); + return; + } + if (authFilterExcludeUrls.contains(servletPath)) { + chain.doFilter(request, response); + return; + } + + // Exclude the urls with suffixes which needn't auth + for (String authFilterExcludeUrlSuffix : authFilterExcludeUrlSuffixes) { + if (StringUtils.isBlank(authFilterExcludeUrlSuffix)) { + continue; + } + + // Add . for url suffix so that we needn't add . in property file + if (!authFilterExcludeUrlSuffix.startsWith(URL_SUFFIX_DOT)) { + authFilterExcludeUrlSuffix = URL_SUFFIX_DOT + authFilterExcludeUrlSuffix; + } + + if (servletPath.endsWith(authFilterExcludeUrlSuffix)) { + chain.doFilter(request, response); + return; + } + } + + AuthService.AuthUser authUser = authService.getAuthUser(httpRequest); + + HttpServletResponse httpResponse = (HttpServletResponse) response; + if (authUser == null) { + // If auth fail, set response status code to 401 + httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + } + else { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java new file mode 100644 index 0000000..bf85b49 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java @@ -0,0 +1,82 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.auth; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +/** + * @author cdfive + * @since 1.6.0 + */ +@Component +@Primary +@ConditionalOnProperty(name = "auth.enabled", matchIfMissing = true) +public class SimpleWebAuthServiceImpl implements AuthService { + + public static final String WEB_SESSION_KEY = "session_sentinel_admin"; + + @Override + public AuthUser getAuthUser(HttpServletRequest request) { + HttpSession session = request.getSession(); + Object sentinelUserObj = session.getAttribute(SimpleWebAuthServiceImpl.WEB_SESSION_KEY); + if (sentinelUserObj != null && sentinelUserObj instanceof AuthUser) { + return (AuthUser) sentinelUserObj; + } + + return null; + } + + public static final class SimpleWebAuthUserImpl implements AuthUser { + + private String username; + + public SimpleWebAuthUserImpl(String username) { + this.username = username; + } + + @Override + public boolean authTarget(String target, PrivilegeType privilegeType) { + return true; + } + + @Override + public boolean isSuperUser() { + return true; + } + + @Override + public String getNickName() { + return username; + } + + @Override + public String getLoginName() { + return username; + } + + @Override + public String getId() { + return username; + } + + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandFailedException.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandFailedException.java new file mode 100644 index 0000000..7408a6b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandFailedException.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.client; + +/** + * @author Eric Zhao + */ +public class CommandFailedException extends RuntimeException { + + public CommandFailedException() { + } + + public CommandFailedException(String message) { + super(message); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandNotFoundException.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandNotFoundException.java new file mode 100644 index 0000000..375af11 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/CommandNotFoundException.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.client; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +public class CommandNotFoundException extends Exception { + + public CommandNotFoundException() { + } + + public CommandNotFoundException(String message) { + super(message); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java new file mode 100644 index 0000000..ac6885a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java @@ -0,0 +1,884 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.client; + +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; +import com.alibaba.csp.sentinel.command.CommandConstants; +import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.*; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterClientInfoVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ClusterClientConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerFlowConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerTransportConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterServerStateVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterStateSimpleEntity; +import com.alibaba.csp.sentinel.dashboard.util.AsyncUtils; +import com.alibaba.csp.sentinel.dashboard.util.VersionUtils; +import com.alibaba.csp.sentinel.slots.block.Rule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.fastjson.JSON; +import org.apache.http.Consts; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +/** + * Communicate with Sentinel client. + * + * @author leyou + */ +@Component +public class SentinelApiClient { + + private static Logger logger = LoggerFactory.getLogger(SentinelApiClient.class); + + private static final Charset DEFAULT_CHARSET = Charset.forName(SentinelConfig.charset()); + + private static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; + + private static final String HTTP_HEADER_CONTENT_TYPE_URLENCODED = ContentType.create(URLEncodedUtils.CONTENT_TYPE) + .toString(); + + private static final String RESOURCE_URL_PATH = "jsonTree"; + + private static final String CLUSTER_NODE_PATH = "clusterNode"; + + private static final String GET_RULES_PATH = "getRules"; + + private static final String SET_RULES_PATH = "setRules"; + + private static final String GET_PARAM_RULE_PATH = "getParamFlowRules"; + + private static final String SET_PARAM_RULE_PATH = "setParamFlowRules"; + + private static final String FETCH_CLUSTER_MODE_PATH = "getClusterMode"; + + private static final String MODIFY_CLUSTER_MODE_PATH = "setClusterMode"; + + private static final String FETCH_CLUSTER_CLIENT_CONFIG_PATH = "cluster/client/fetchConfig"; + + private static final String MODIFY_CLUSTER_CLIENT_CONFIG_PATH = "cluster/client/modifyConfig"; + + private static final String FETCH_CLUSTER_SERVER_BASIC_INFO_PATH = "cluster/server/info"; + + private static final String MODIFY_CLUSTER_SERVER_TRANSPORT_CONFIG_PATH = "cluster/server/modifyTransportConfig"; + + private static final String MODIFY_CLUSTER_SERVER_FLOW_CONFIG_PATH = "cluster/server/modifyFlowConfig"; + + private static final String MODIFY_CLUSTER_SERVER_NAMESPACE_SET_PATH = "cluster/server/modifyNamespaceSet"; + + private static final String FETCH_GATEWAY_API_PATH = "gateway/getApiDefinitions"; + + private static final String MODIFY_GATEWAY_API_PATH = "gateway/updateApiDefinitions"; + + private static final String FETCH_GATEWAY_FLOW_RULE_PATH = "gateway/getRules"; + + private static final String MODIFY_GATEWAY_FLOW_RULE_PATH = "gateway/updateRules"; + + private static final String FLOW_RULE_TYPE = "flow"; + + private static final String DEGRADE_RULE_TYPE = "degrade"; + + private static final String SYSTEM_RULE_TYPE = "system"; + + private static final String AUTHORITY_TYPE = "authority"; + + private CloseableHttpAsyncClient httpClient; + + private static final SentinelVersion version160 = new SentinelVersion(1, 6, 0); + + private static final SentinelVersion version171 = new SentinelVersion(1, 7, 1); + + @Autowired + private AppManagement appManagement; + + public SentinelApiClient() { + IOReactorConfig ioConfig = IOReactorConfig.custom().setConnectTimeout(3000).setSoTimeout(10000) + .setIoThreadCount(Runtime.getRuntime().availableProcessors() * 2).build(); + httpClient = HttpAsyncClients.custom().setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(final String method) { + return false; + } + }).setMaxConnTotal(4000).setMaxConnPerRoute(1000).setDefaultIOReactorConfig(ioConfig).build(); + httpClient.start(); + } + + private boolean isSuccess(int statusCode) { + return statusCode >= 200 && statusCode < 300; + } + + private boolean isCommandNotFound(int statusCode, String body) { + return statusCode == 400 && StringUtil.isNotEmpty(body) + && body.contains(CommandConstants.MSG_UNKNOWN_COMMAND_PREFIX); + } + + protected boolean isSupportPost(String app, String ip, int port) { + return StringUtil.isNotEmpty(app) + && Optional.ofNullable(appManagement.getDetailApp(app)).flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()).map(v -> v.greaterOrEqual(version160))) + .orElse(false); + } + + /** + * Check wheter target instance (identified by tuple of app-ip:port) supports the form + * of "xxxxx; xx=xx" in "Content-Type" header. + * @param app target app name + * @param ip target node's address + * @param port target node's port + */ + protected boolean isSupportEnhancedContentType(String app, String ip, int port) { + return StringUtil.isNotEmpty(app) + && Optional.ofNullable(appManagement.getDetailApp(app)).flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()).map(v -> v.greaterOrEqual(version171))) + .orElse(false); + } + + private StringBuilder queryString(Map params) { + StringBuilder queryStringBuilder = new StringBuilder(); + for (Entry entry : params.entrySet()) { + if (StringUtil.isEmpty(entry.getValue())) { + continue; + } + String name = urlEncode(entry.getKey()); + String value = urlEncode(entry.getValue()); + if (name != null && value != null) { + if (queryStringBuilder.length() > 0) { + queryStringBuilder.append('&'); + } + queryStringBuilder.append(name).append('=').append(value); + } + } + return queryStringBuilder; + } + + /** + * Build an `HttpUriRequest` in POST way. + * @param url + * @param params + * @param supportEnhancedContentType see + * {@link #isSupportEnhancedContentType(String, String, int)} + * @return + */ + protected static HttpUriRequest postRequest(String url, Map params, + boolean supportEnhancedContentType) { + HttpPost httpPost = new HttpPost(url); + if (params != null && params.size() > 0) { + List list = new ArrayList<>(params.size()); + for (Entry entry : params.entrySet()) { + list.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + httpPost.setEntity(new UrlEncodedFormEntity(list, Consts.UTF_8)); + if (!supportEnhancedContentType) { + httpPost.setHeader(HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_CONTENT_TYPE_URLENCODED); + } + } + return httpPost; + } + + private String urlEncode(String str) { + try { + return URLEncoder.encode(str, DEFAULT_CHARSET.name()); + } + catch (UnsupportedEncodingException e) { + logger.info("encode string error: {}", str, e); + return null; + } + } + + private String getBody(HttpResponse response) throws Exception { + Charset charset = null; + try { + String contentTypeStr = response.getFirstHeader(HTTP_HEADER_CONTENT_TYPE).getValue(); + if (StringUtil.isNotEmpty(contentTypeStr)) { + ContentType contentType = ContentType.parse(contentTypeStr); + charset = contentType.getCharset(); + } + } + catch (Exception ignore) { + } + return EntityUtils.toString(response.getEntity(), charset != null ? charset : DEFAULT_CHARSET); + } + + /** + * With no param + * @param ip + * @param port + * @param api + * @return + */ + private CompletableFuture executeCommand(String ip, int port, String api, boolean useHttpPost) { + return executeCommand(ip, port, api, null, useHttpPost); + } + + /** + * No app specified, force to GET + * @param ip + * @param port + * @param api + * @param params + * @return + */ + private CompletableFuture executeCommand(String ip, int port, String api, Map params, + boolean useHttpPost) { + return executeCommand(null, ip, port, api, params, useHttpPost); + } + + /** + * Prefer to execute request using POST + * @param app + * @param ip + * @param port + * @param api + * @param params + * @return + */ + private CompletableFuture executeCommand(String app, String ip, int port, String api, + Map params, boolean useHttpPost) { + CompletableFuture future = new CompletableFuture<>(); + if (StringUtil.isBlank(ip) || StringUtil.isBlank(api)) { + future.completeExceptionally(new IllegalArgumentException("Bad URL or command name")); + return future; + } + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append("http://"); + urlBuilder.append(ip).append(':').append(port).append('/').append(api); + if (params == null) { + params = Collections.emptyMap(); + } + if (!useHttpPost || !isSupportPost(app, ip, port)) { + // Using GET in older versions, append parameters after url + if (!params.isEmpty()) { + if (urlBuilder.indexOf("?") == -1) { + urlBuilder.append('?'); + } + else { + urlBuilder.append('&'); + } + urlBuilder.append(queryString(params)); + } + return executeCommand(new HttpGet(urlBuilder.toString())); + } + else { + // Using POST + return executeCommand( + postRequest(urlBuilder.toString(), params, isSupportEnhancedContentType(app, ip, port))); + } + } + + private CompletableFuture executeCommand(HttpUriRequest request) { + CompletableFuture future = new CompletableFuture<>(); + httpClient.execute(request, new FutureCallback() { + @Override + public void completed(final HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + try { + String value = getBody(response); + if (isSuccess(statusCode)) { + future.complete(value); + } + else { + if (isCommandNotFound(statusCode, value)) { + future.completeExceptionally(new CommandNotFoundException(request.getURI().getPath())); + } + else { + future.completeExceptionally(new CommandFailedException(value)); + } + } + + } + catch (Exception ex) { + future.completeExceptionally(ex); + logger.error("HTTP request failed: {}", request.getURI().toString(), ex); + } + } + + @Override + public void failed(final Exception ex) { + future.completeExceptionally(ex); + logger.error("HTTP request failed: {}", request.getURI().toString(), ex); + } + + @Override + public void cancelled() { + future.complete(null); + } + }); + return future; + } + + public void close() throws Exception { + httpClient.close(); + } + + @Nullable + private CompletableFuture> fetchItemsAsync(String ip, int port, String api, String type, + Class ruleType) { + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + Map params = null; + if (StringUtil.isNotEmpty(type)) { + params = new HashMap<>(1); + params.put("type", type); + } + return executeCommand(ip, port, api, params, false).thenApply(json -> JSON.parseArray(json, ruleType)); + } + + @Nullable + private List fetchItems(String ip, int port, String api, String type, Class ruleType) { + try { + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + Map params = null; + if (StringUtil.isNotEmpty(type)) { + params = new HashMap<>(1); + params.put("type", type); + } + return fetchItemsAsync(ip, port, api, type, ruleType).get(); + } + catch (InterruptedException | ExecutionException e) { + logger.error("Error when fetching items from api: {} -> {}", api, type, e); + return null; + } + catch (Exception e) { + logger.error("Error when fetching items: {} -> {}", api, type, e); + return null; + } + } + + private List fetchRules(String ip, int port, String type, Class ruleType) { + return fetchItems(ip, port, GET_RULES_PATH, type, ruleType); + } + + private boolean setRules(String app, String ip, int port, String type, List entities) { + if (entities == null) { + return true; + } + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString(entities.stream().map(r -> r.toRule()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("type", type); + params.put("data", data); + String result = executeCommand(app, ip, port, SET_RULES_PATH, params, true).get(); + logger.info("setRules result: {}, type={}", result, type); + return true; + } + catch (InterruptedException e) { + logger.warn("setRules API failed: {}", type, e); + return false; + } + catch (ExecutionException e) { + logger.warn("setRules API failed: {}", type, e.getCause()); + return false; + } + catch (Exception e) { + logger.error("setRules API failed, type={}", type, e); + return false; + } + } + + private CompletableFuture setRulesAsync(String app, String ip, int port, String type, + List entities) { + try { + AssertUtil.notNull(entities, "rules cannot be null"); + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString(entities.stream().map(r -> r.toRule()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("type", type); + params.put("data", data); + return executeCommand(app, ip, port, SET_RULES_PATH, params, true).thenCompose(r -> { + if ("success".equalsIgnoreCase(r.trim())) { + return CompletableFuture.completedFuture(null); + } + return AsyncUtils.newFailedFuture(new CommandFailedException(r)); + }); + } + catch (Exception e) { + logger.error("setRulesAsync API failed, type={}", type, e); + return AsyncUtils.newFailedFuture(e); + } + } + + public List fetchResourceOfMachine(String ip, int port, String type) { + return fetchItems(ip, port, RESOURCE_URL_PATH, type, NodeVo.class); + } + + /** + * Fetch cluster node. + * @param ip ip to fetch + * @param port port of the ip + * @param includeZero whether zero value should in the result list. + * @return + */ + public List fetchClusterNodeOfMachine(String ip, int port, boolean includeZero) { + String type = "notZero"; + if (includeZero) { + type = "zero"; + } + return fetchItems(ip, port, CLUSTER_NODE_PATH, type, NodeVo.class); + } + + public List fetchFlowRuleOfMachine(String app, String ip, int port) { + List rules = fetchRules(ip, port, FLOW_RULE_TYPE, FlowRule.class); + if (rules != null) { + return rules.stream().map(rule -> FlowRuleEntity.fromFlowRule(app, ip, port, rule)) + .collect(Collectors.toList()); + } + else { + return null; + } + } + + public List fetchDegradeRuleOfMachine(String app, String ip, int port) { + List rules = fetchRules(ip, port, DEGRADE_RULE_TYPE, DegradeRule.class); + if (rules != null) { + return rules.stream().map(rule -> DegradeRuleEntity.fromDegradeRule(app, ip, port, rule)) + .collect(Collectors.toList()); + } + else { + return null; + } + } + + public List fetchSystemRuleOfMachine(String app, String ip, int port) { + List rules = fetchRules(ip, port, SYSTEM_RULE_TYPE, SystemRule.class); + if (rules != null) { + return rules.stream().map(rule -> SystemRuleEntity.fromSystemRule(app, ip, port, rule)) + .collect(Collectors.toList()); + } + else { + return null; + } + } + + /** + * Fetch all parameter flow rules from provided machine. + * @param app application name + * @param ip machine client IP + * @param port machine client port + * @return all retrieved parameter flow rules + * @since 0.2.1 + */ + public CompletableFuture> fetchParamFlowRulesOfMachine(String app, String ip, int port) { + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + return fetchItemsAsync(ip, port, GET_PARAM_RULE_PATH, null, ParamFlowRule.class) + .thenApply(rules -> rules.stream().map(e -> ParamFlowRuleEntity.fromAuthorityRule(app, ip, port, e)) + .collect(Collectors.toList())); + } + catch (Exception e) { + logger.error("Error when fetching parameter flow rules", e); + return AsyncUtils.newFailedFuture(e); + } + } + + /** + * Fetch all authority rules from provided machine. + * @param app application name + * @param ip machine client IP + * @param port machine client port + * @return all retrieved authority rules + * @since 0.2.1 + */ + public List fetchAuthorityRulesOfMachine(String app, String ip, int port) { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + Map params = new HashMap<>(1); + params.put("type", AUTHORITY_TYPE); + List rules = fetchRules(ip, port, AUTHORITY_TYPE, AuthorityRule.class); + return Optional.ofNullable(rules).map(r -> r.stream() + .map(e -> AuthorityRuleEntity.fromAuthorityRule(app, ip, port, e)).collect(Collectors.toList())) + .orElse(null); + } + + /** + * set rules of the machine. rules == null will return immediately; rules.isEmpty() + * means setting the rules to empty. + * @param app + * @param ip + * @param port + * @param rules + * @return whether successfully set the rules. + */ + public boolean setFlowRuleOfMachine(String app, String ip, int port, List rules) { + return setRules(app, ip, port, FLOW_RULE_TYPE, rules); + } + + public CompletableFuture setFlowRuleOfMachineAsync(String app, String ip, int port, + List rules) { + return setRulesAsync(app, ip, port, FLOW_RULE_TYPE, rules); + } + + /** + * set rules of the machine. rules == null will return immediately; rules.isEmpty() + * means setting the rules to empty. + * @param app + * @param ip + * @param port + * @param rules + * @return whether successfully set the rules. + */ + public boolean setDegradeRuleOfMachine(String app, String ip, int port, List rules) { + return setRules(app, ip, port, DEGRADE_RULE_TYPE, rules); + } + + /** + * set rules of the machine. rules == null will return immediately; rules.isEmpty() + * means setting the rules to empty. + * @param app + * @param ip + * @param port + * @param rules + * @return whether successfully set the rules. + */ + public boolean setSystemRuleOfMachine(String app, String ip, int port, List rules) { + return setRules(app, ip, port, SYSTEM_RULE_TYPE, rules); + } + + public boolean setAuthorityRuleOfMachine(String app, String ip, int port, List rules) { + return setRules(app, ip, port, AUTHORITY_TYPE, rules); + } + + public CompletableFuture setParamFlowRuleOfMachine(String app, String ip, int port, + List rules) { + if (rules == null) { + return CompletableFuture.completedFuture(null); + } + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + String data = JSON + .toJSONString(rules.stream().map(ParamFlowRuleEntity::getRule).collect(Collectors.toList())); + Map params = new HashMap<>(1); + params.put("data", data); + return executeCommand(app, ip, port, SET_PARAM_RULE_PATH, params, true).thenCompose(e -> { + if (CommandConstants.MSG_SUCCESS.equals(e)) { + return CompletableFuture.completedFuture(null); + } + else { + logger.warn("Push parameter flow rules to client failed: " + e); + return AsyncUtils.newFailedFuture(new RuntimeException(e)); + } + }); + } + catch (Exception ex) { + logger.warn("Error when setting parameter flow rule", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + // Cluster related + + public CompletableFuture fetchClusterMode(String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + return executeCommand(ip, port, FETCH_CLUSTER_MODE_PATH, false) + .thenApply(r -> JSON.parseObject(r, ClusterStateSimpleEntity.class)); + } + catch (Exception ex) { + logger.warn("Error when fetching cluster mode", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture modifyClusterMode(String ip, int port, int mode) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + Map params = new HashMap<>(1); + params.put("mode", String.valueOf(mode)); + return executeCommand(ip, port, MODIFY_CLUSTER_MODE_PATH, params, false).thenCompose(e -> { + if (CommandConstants.MSG_SUCCESS.equals(e)) { + return CompletableFuture.completedFuture(null); + } + else { + logger.warn("Error when modifying cluster mode: " + e); + return AsyncUtils.newFailedFuture(new RuntimeException(e)); + } + }); + } + catch (Exception ex) { + logger.warn("Error when modifying cluster mode", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture fetchClusterClientInfoAndConfig(String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + return executeCommand(ip, port, FETCH_CLUSTER_CLIENT_CONFIG_PATH, false) + .thenApply(r -> JSON.parseObject(r, ClusterClientInfoVO.class)); + } + catch (Exception ex) { + logger.warn("Error when fetching cluster client config", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture modifyClusterClientConfig(String app, String ip, int port, + ClusterClientConfig config) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + Map params = new HashMap<>(1); + params.put("data", JSON.toJSONString(config)); + return executeCommand(app, ip, port, MODIFY_CLUSTER_CLIENT_CONFIG_PATH, params, true).thenCompose(e -> { + if (CommandConstants.MSG_SUCCESS.equals(e)) { + return CompletableFuture.completedFuture(null); + } + else { + logger.warn("Error when modifying cluster client config: " + e); + return AsyncUtils.newFailedFuture(new RuntimeException(e)); + } + }); + } + catch (Exception ex) { + logger.warn("Error when modifying cluster client config", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture modifyClusterServerFlowConfig(String app, String ip, int port, + ServerFlowConfig config) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + Map params = new HashMap<>(1); + params.put("data", JSON.toJSONString(config)); + return executeCommand(app, ip, port, MODIFY_CLUSTER_SERVER_FLOW_CONFIG_PATH, params, true) + .thenCompose(e -> { + if (CommandConstants.MSG_SUCCESS.equals(e)) { + return CompletableFuture.completedFuture(null); + } + else { + logger.warn("Error when modifying cluster server flow config: " + e); + return AsyncUtils.newFailedFuture(new RuntimeException(e)); + } + }); + } + catch (Exception ex) { + logger.warn("Error when modifying cluster server flow config", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture modifyClusterServerTransportConfig(String app, String ip, int port, + ServerTransportConfig config) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + Map params = new HashMap<>(2); + params.put("port", config.getPort().toString()); + params.put("idleSeconds", config.getIdleSeconds().toString()); + return executeCommand(app, ip, port, MODIFY_CLUSTER_SERVER_TRANSPORT_CONFIG_PATH, params, false) + .thenCompose(e -> { + if (CommandConstants.MSG_SUCCESS.equals(e)) { + return CompletableFuture.completedFuture(null); + } + else { + logger.warn("Error when modifying cluster server transport config: " + e); + return AsyncUtils.newFailedFuture(new RuntimeException(e)); + } + }); + } + catch (Exception ex) { + logger.warn("Error when modifying cluster server transport config", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture modifyClusterServerNamespaceSet(String app, String ip, int port, Set set) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + Map params = new HashMap<>(1); + params.put("data", JSON.toJSONString(set)); + return executeCommand(app, ip, port, MODIFY_CLUSTER_SERVER_NAMESPACE_SET_PATH, params, true) + .thenCompose(e -> { + if (CommandConstants.MSG_SUCCESS.equals(e)) { + return CompletableFuture.completedFuture(null); + } + else { + logger.warn("Error when modifying cluster server NamespaceSet", e); + return AsyncUtils.newFailedFuture(new RuntimeException(e)); + } + }); + } + catch (Exception ex) { + logger.warn("Error when modifying cluster server NamespaceSet", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture fetchClusterServerBasicInfo(String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + try { + return executeCommand(ip, port, FETCH_CLUSTER_SERVER_BASIC_INFO_PATH, false) + .thenApply(r -> JSON.parseObject(r, ClusterServerStateVO.class)); + } + catch (Exception ex) { + logger.warn("Error when fetching cluster sever all config and basic info", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public CompletableFuture> fetchApis(String app, String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + + try { + return executeCommand(ip, port, FETCH_GATEWAY_API_PATH, false).thenApply(r -> { + List entities = JSON.parseArray(r, ApiDefinitionEntity.class); + if (entities != null) { + for (ApiDefinitionEntity entity : entities) { + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + } + } + return entities; + }); + } + catch (Exception ex) { + logger.warn("Error when fetching gateway apis", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public boolean modifyApis(String app, String ip, int port, List apis) { + if (apis == null) { + return true; + } + + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString(apis.stream().map(r -> r.toApiDefinition()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("data", data); + String result = executeCommand(app, ip, port, MODIFY_GATEWAY_API_PATH, params, true).get(); + logger.info("Modify gateway apis: {}", result); + return true; + } + catch (Exception e) { + logger.warn("Error when modifying gateway apis", e); + return false; + } + } + + public CompletableFuture> fetchGatewayFlowRules(String app, String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + + try { + return executeCommand(ip, port, FETCH_GATEWAY_FLOW_RULE_PATH, false).thenApply(r -> { + List gatewayFlowRules = JSON.parseArray(r, GatewayFlowRule.class); + List entities = gatewayFlowRules.stream() + .map(rule -> GatewayFlowRuleEntity.fromGatewayFlowRule(app, ip, port, rule)) + .collect(Collectors.toList()); + return entities; + }); + } + catch (Exception ex) { + logger.warn("Error when fetching gateway flow rules", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public boolean modifyGatewayFlowRules(String app, String ip, int port, List rules) { + if (rules == null) { + return true; + } + + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON + .toJSONString(rules.stream().map(r -> r.toGatewayFlowRule()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("data", data); + String result = executeCommand(app, ip, port, MODIFY_GATEWAY_FLOW_RULE_PATH, params, true).get(); + logger.info("Modify gateway flow rules: {}", result); + return true; + } + catch (Exception e) { + logger.warn("Error when modifying gateway apis", e); + return false; + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/DashboardConfig.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/DashboardConfig.java new file mode 100644 index 0000000..1684e5f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/DashboardConfig.java @@ -0,0 +1,149 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.config; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.math.NumberUtils; +import org.springframework.lang.NonNull; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + *

+ * Dashboard local config support. + *

+ *

+ * Dashboard supports configuration loading by several ways by order:
+ * 1. System.properties
+ * 2. Env + *

+ * + * @author jason + * @since 1.5.0 + */ +public class DashboardConfig { + + public static final int DEFAULT_MACHINE_HEALTHY_TIMEOUT_MS = 60_000; + + /** + * Login username + */ + public static final String CONFIG_AUTH_USERNAME = "sentinel.dashboard.auth.username"; + + /** + * Login password + */ + public static final String CONFIG_AUTH_PASSWORD = "sentinel.dashboard.auth.password"; + + /** + * Hide application name in sidebar when it has no healthy machines after specific + * period in millisecond. + */ + public static final String CONFIG_HIDE_APP_NO_MACHINE_MILLIS = "sentinel.dashboard.app.hideAppNoMachineMillis"; + + /** + * Remove application when it has no healthy machines after specific period in + * millisecond. + */ + public static final String CONFIG_REMOVE_APP_NO_MACHINE_MILLIS = "sentinel.dashboard.removeAppNoMachineMillis"; + + /** + * Timeout + */ + public static final String CONFIG_UNHEALTHY_MACHINE_MILLIS = "sentinel.dashboard.unhealthyMachineMillis"; + + /** + * Auto remove unhealthy machine after specific period in millisecond. + */ + public static final String CONFIG_AUTO_REMOVE_MACHINE_MILLIS = "sentinel.dashboard.autoRemoveMachineMillis"; + + private static final ConcurrentMap cacheMap = new ConcurrentHashMap<>(); + + @NonNull + private static String getConfig(String name) { + // env + String val = System.getenv(name); + if (StringUtils.isNotEmpty(val)) { + return val; + } + // properties + val = System.getProperty(name); + if (StringUtils.isNotEmpty(val)) { + return val; + } + return ""; + } + + protected static String getConfigStr(String name) { + if (cacheMap.containsKey(name)) { + return (String) cacheMap.get(name); + } + + String val = getConfig(name); + + if (StringUtils.isBlank(val)) { + return null; + } + + cacheMap.put(name, val); + return val; + } + + protected static int getConfigInt(String name, int defaultVal, int minVal) { + if (cacheMap.containsKey(name)) { + return (int) cacheMap.get(name); + } + int val = NumberUtils.toInt(getConfig(name)); + if (val == 0) { + val = defaultVal; + } + else if (val < minVal) { + val = minVal; + } + cacheMap.put(name, val); + return val; + } + + public static String getAuthUsername() { + return getConfigStr(CONFIG_AUTH_USERNAME); + } + + public static String getAuthPassword() { + return getConfigStr(CONFIG_AUTH_PASSWORD); + } + + public static int getHideAppNoMachineMillis() { + return getConfigInt(CONFIG_HIDE_APP_NO_MACHINE_MILLIS, 0, 60000); + } + + public static int getRemoveAppNoMachineMillis() { + return getConfigInt(CONFIG_REMOVE_APP_NO_MACHINE_MILLIS, 0, 120000); + } + + public static int getAutoRemoveMachineMillis() { + return getConfigInt(CONFIG_AUTO_REMOVE_MACHINE_MILLIS, 0, 300000); + } + + public static int getUnhealthyMachineMillis() { + return getConfigInt(CONFIG_UNHEALTHY_MACHINE_MILLIS, DEFAULT_MACHINE_HEALTHY_TIMEOUT_MS, 30000); + } + + public static void clearCache() { + cacheMap.clear(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java new file mode 100644 index 0000000..7f886ad --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java @@ -0,0 +1,115 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.config; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.dashboard.auth.AuthorizationInterceptor; +import com.alibaba.csp.sentinel.dashboard.auth.LoginAuthenticationFilter; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.PostConstruct; +import javax.servlet.Filter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author leyou + */ +@Configuration(proxyBeanMethods = false) +public class WebConfig implements WebMvcConfigurer { + + private final Logger logger = LoggerFactory.getLogger(WebConfig.class); + + @Autowired + private LoginAuthenticationFilter loginAuthenticationFilter; + + @Autowired + private AuthorizationInterceptor authorizationInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**").addResourceLocations("classpath:/resources/"); + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("forward:/index.htm"); + } + + /** + * Add {@link CommonFilter} to the server, this is the simplest way to use Sentinel + * for Web application. + */ + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new CommonFilter()); + registration.addUrlPatterns("/*"); + registration.setName("sentinelFilter"); + registration.setOrder(1); + // If this is enabled, the entrance of all Web URL resources will be unified as a + // single context name. + // In most scenarios that's enough, and it could reduce the memory footprint. + registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "true"); + + logger.info("Sentinel servlet CommonFilter registered"); + + return registration; + } + + @PostConstruct + public void doInit() { + Set suffixSet = new HashSet<>(Arrays.asList(".js", ".css", ".html", ".ico", ".txt", ".woff", ".woff2")); + // Example: register a UrlCleaner to exclude URLs of common static resources. + WebCallbackManager.setUrlCleaner(url -> { + if (StringUtil.isEmpty(url)) { + return url; + } + if (suffixSet.stream().anyMatch(url::endsWith)) { + return null; + } + return url; + }); + } + + @Bean + public FilterRegistrationBean authenticationFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(loginAuthenticationFilter); + registration.addUrlPatterns("/*"); + registration.setName("authenticationFilter"); + registration.setOrder(0); + return registration; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java new file mode 100644 index 0000000..b372453 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java @@ -0,0 +1,81 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.discovery.AppInfo; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.MachineInfoVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * @author Carpenter Lee + */ +@RestController +@RequestMapping(value = "/app") +public class AppController { + + @Autowired + private AppManagement appManagement; + + @GetMapping("/names.json") + public Result> queryApps(HttpServletRequest request) { + return Result.ofSuccess(appManagement.getAppNames()); + } + + @GetMapping("/briefinfos.json") + public Result> queryAppInfos(HttpServletRequest request) { + List list = new ArrayList<>(appManagement.getBriefApps()); + Collections.sort(list, Comparator.comparing(AppInfo::getApp)); + return Result.ofSuccess(list); + } + + @GetMapping(value = "/{app}/machines.json") + public Result> getMachinesByApp(@PathVariable("app") String app) { + AppInfo appInfo = appManagement.getDetailApp(app); + if (appInfo == null) { + return Result.ofSuccess(null); + } + List list = new ArrayList<>(appInfo.getMachines()); + Collections.sort(list, Comparator.comparing(MachineInfo::getApp).thenComparing(MachineInfo::getIp) + .thenComparingInt(MachineInfo::getPort)); + return Result.ofSuccess(MachineInfoVo.fromMachineInfoList(list)); + } + + @RequestMapping(value = "/{app}/machine/remove.json") + public Result removeMachineById(@PathVariable("app") String app, @RequestParam(name = "ip") String ip, + @RequestParam(name = "port") int port) { + AppInfo appInfo = appManagement.getDetailApp(app); + if (appInfo == null) { + return Result.ofSuccess(null); + } + if (appManagement.removeMachine(app, ip, port)) { + return Result.ofSuccessMsg("success"); + } + else { + return Result.ofFail(1, "remove failed"); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java new file mode 100644 index 0000000..0954f38 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java @@ -0,0 +1,94 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.auth.SimpleWebAuthServiceImpl; +import com.alibaba.csp.sentinel.dashboard.config.DashboardConfig; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author cdfive + * @since 1.6.0 + */ +@RestController +@RequestMapping("/auth") +public class AuthController { + + private static final Logger LOGGER = LoggerFactory.getLogger(AuthController.class); + + @Value("${auth.username:sentinel}") + private String authUsername; + + @Value("${auth.password:sentinel}") + private String authPassword; + + @Autowired + private AuthService authService; + + @PostMapping("/login") + public Result login(HttpServletRequest request, String username, String password) { + if (StringUtils.isNotBlank(DashboardConfig.getAuthUsername())) { + authUsername = DashboardConfig.getAuthUsername(); + } + + if (StringUtils.isNotBlank(DashboardConfig.getAuthPassword())) { + authPassword = DashboardConfig.getAuthPassword(); + } + + /* + * If auth.username or auth.password is blank(set in application.properties or VM + * arguments), auth will pass, as the front side validate the input which can't be + * blank, so user can input any username or password(both are not blank) to login + * in that case. + */ + if (StringUtils.isNotBlank(authUsername) && !authUsername.equals(username) + || StringUtils.isNotBlank(authPassword) && !authPassword.equals(password)) { + LOGGER.error("Login failed: Invalid username or password, username=" + username); + return Result.ofFail(-1, "Invalid username or password"); + } + + AuthService.AuthUser authUser = new SimpleWebAuthServiceImpl.SimpleWebAuthUserImpl(username); + request.getSession().setAttribute(SimpleWebAuthServiceImpl.WEB_SESSION_KEY, authUser); + return Result.ofSuccess(authUser); + } + + @PostMapping(value = "/logout") + public Result logout(HttpServletRequest request) { + request.getSession().invalidate(); + return Result.ofSuccess(null); + } + + @PostMapping(value = "/check") + public Result check(HttpServletRequest request) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + if (authUser == null) { + return Result.ofFail(-1, "Not logged in"); + } + return Result.ofSuccess(authUser); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java new file mode 100644 index 0000000..1d14a88 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java @@ -0,0 +1,186 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +@RestController +@RequestMapping(value = "/authority") +public class AuthorityRuleController { + + private final Logger logger = LoggerFactory.getLogger(AuthorityRuleController.class); + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private RuleRepository repository; + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, + @RequestParam String ip, @RequestParam Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip cannot be null or empty"); + } + if (port == null || port <= 0) { + return Result.ofFail(-1, "Invalid parameter: port"); + } + try { + List rules = sentinelApiClient.fetchAuthorityRulesOfMachine(app, ip, port); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("Error when querying authority rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private Result checkEntityInternal(AuthorityRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "bad rule body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "port can't be null"); + } + if (entity.getRule() == null) { + return Result.ofFail(-1, "rule can't be null"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource name cannot be null or empty"); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp should be valid"); + } + if (entity.getStrategy() != RuleConstant.AUTHORITY_WHITE + && entity.getStrategy() != RuleConstant.AUTHORITY_BLACK) { + return Result.ofFail(-1, "Unknown strategy (must be blacklist or whitelist)"); + } + return null; + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddAuthorityRule(@RequestBody AuthorityRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(null); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } + catch (Throwable throwable) { + logger.error("Failed to add authority rule", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.info("Publish authority rules failed after rule add"); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, + @RequestBody AuthorityRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(null); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + if (entity == null) { + return Result.ofFail(-1, "Failed to save authority rule"); + } + } + catch (Throwable throwable) { + logger.error("Failed to save authority rule", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.info("Publish authority rules failed after rule update"); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id cannot be null"); + } + AuthorityRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + } + catch (Exception e) { + return Result.ofFail(-1, e.getMessage()); + } + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.error("Publish authority rules failed after rule delete"); + } + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.setAuthorityRuleOfMachine(app, ip, port, rules); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java new file mode 100644 index 0000000..3c0cc84 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java @@ -0,0 +1,219 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; + +/** + * Controller regarding APIs of degrade rules. Refactored since 1.8.0. + * + * @author Carpenter Lee + * @author Eric Zhao + */ +@RestController +@RequestMapping("/degrade") +public class DegradeController { + + private final Logger logger = LoggerFactory.getLogger(DegradeController.class); + + @Autowired + private RuleRepository repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @GetMapping("/rules.json") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + try { + List rules = sentinelApiClient.fetchDegradeRuleOfMachine(app, ip, port); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("queryApps error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddRule(@RequestBody DegradeRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } + catch (Throwable t) { + logger.error("Failed to add new degrade rule, app={}, ip={}", entity.getApp(), entity.getIp(), t); + return Result.ofThrowable(-1, t); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.warn("Publish degrade rules failed, app={}", entity.getApp()); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateRule(@PathVariable("id") Long id, @RequestBody DegradeRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "id can't be null or negative"); + } + DegradeRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "Degrade rule does not exist, id=" + id); + } + entity.setApp(oldEntity.getApp()); + entity.setIp(oldEntity.getIp()); + entity.setPort(oldEntity.getPort()); + entity.setId(oldEntity.getId()); + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(new Date()); + try { + entity = repository.save(entity); + } + catch (Throwable t) { + logger.error("Failed to save degrade rule, id={}, rule={}", id, entity, t); + return Result.ofThrowable(-1, t); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.warn("Publish degrade rules failed, app={}", entity.getApp()); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result delete(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + DegradeRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } + catch (Throwable throwable) { + logger.error("Failed to delete degrade rule, id={}", id, throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("Publish degrade rules failed, app={}", oldEntity.getApp()); + } + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.setDegradeRuleOfMachine(app, ip, port, rules); + } + + private Result checkEntityInternal(DegradeRuleEntity entity) { + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be blank"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "invalid port: " + entity.getPort()); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + Double threshold = entity.getCount(); + if (threshold == null || threshold < 0) { + return Result.ofFail(-1, "invalid threshold: " + threshold); + } + Integer recoveryTimeoutSec = entity.getTimeWindow(); + if (recoveryTimeoutSec == null || recoveryTimeoutSec <= 0) { + return Result.ofFail(-1, "recoveryTimeout should be positive"); + } + Integer strategy = entity.getGrade(); + if (strategy == null) { + return Result.ofFail(-1, "circuit breaker strategy cannot be null"); + } + if (strategy < CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType() + || strategy > RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) { + return Result.ofFail(-1, "Invalid circuit breaker strategy: " + strategy); + } + if (entity.getMinRequestAmount() == null || entity.getMinRequestAmount() <= 0) { + return Result.ofFail(-1, "Invalid minRequestAmount"); + } + if (entity.getStatIntervalMs() == null || entity.getStatIntervalMs() <= 0) { + return Result.ofFail(-1, "Invalid statInterval"); + } + if (strategy == RuleConstant.DEGRADE_GRADE_RT) { + Double slowRatio = entity.getSlowRatioThreshold(); + if (slowRatio == null) { + return Result.ofFail(-1, "SlowRatioThreshold is required for slow request ratio strategy"); + } + else if (slowRatio < 0 || slowRatio > 1) { + return Result.ofFail(-1, "SlowRatioThreshold should be in range: [0.0, 1.0]"); + } + } + else if (strategy == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { + if (threshold > 1) { + return Result.ofFail(-1, "Ratio threshold should be in range: [0.0, 1.0]"); + } + } + return null; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DemoController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DemoController.java new file mode 100644 index 0000000..0a62073 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DemoController.java @@ -0,0 +1,141 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Controller +@RequestMapping(value = "/demo", produces = MediaType.APPLICATION_JSON_VALUE) +public class DemoController { + + Logger logger = LoggerFactory.getLogger(MachineRegistryController.class); + + @RequestMapping("/greeting") + public String greeting() { + return "index"; + } + + @RequestMapping("/link") + @ResponseBody + public String link() throws BlockException { + + Entry entry = SphU.entry("head1", EntryType.IN); + + Entry entry1 = SphU.entry("head2", EntryType.IN); + Entry entry2 = SphU.entry("head3", EntryType.IN); + Entry entry3 = SphU.entry("head4", EntryType.IN); + + entry3.exit(); + entry2.exit(); + entry1.exit(); + entry.exit(); + return "successfully create a call link"; + } + + @RequestMapping("/loop") + @ResponseBody + public String loop(String name, int time) throws BlockException { + for (int i = 0; i < 10; i++) { + Thread timer = new Thread(new RunTask(name, time, false)); + timer.setName("false"); + timer.start(); + } + return "successfully create a loop thread"; + } + + @RequestMapping("/slow") + @ResponseBody + public String slow(String name, int time) throws BlockException { + for (int i = 0; i < 10; i++) { + Thread timer = new Thread(new RunTask(name, time, true)); + timer.setName("false"); + timer.start(); + } + return "successfully create a loop thread"; + } + + static class RunTask implements Runnable { + + int time; + + boolean stop = false; + + String name; + + boolean slow = false; + + public RunTask(String name, int time, boolean slow) { + super(); + this.time = time; + this.name = name; + this.slow = slow; + } + + @Override + public void run() { + long startTime = System.currentTimeMillis(); + ContextUtil.enter(String.valueOf(startTime)); + while (!stop) { + + long now = System.currentTimeMillis(); + if (now - startTime > time * 1000) { + stop = true; + } + Entry e1 = null; + try { + e1 = SphU.entry(name); + + if (slow) { + TimeUnit.MILLISECONDS.sleep(3000); + } + + } + catch (Exception e) { + } + finally { + if (e1 != null) { + e1.exit(); + } + } + Random random2 = new Random(); + try { + TimeUnit.MILLISECONDS.sleep(random2.nextInt(200)); + } + catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + ContextUtil.exit(); + } + + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java new file mode 100644 index 0000000..47d8d1c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java @@ -0,0 +1,267 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * Flow rule controller. + * + * @author leyou + * @author Eric Zhao + */ +@RestController +@RequestMapping(value = "/v1/flow") +public class FlowControllerV1 { + + private final Logger logger = LoggerFactory.getLogger(FlowControllerV1.class); + + @Autowired + private InMemoryRuleRepositoryAdapter repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(@RequestParam String app, @RequestParam String ip, + @RequestParam Integer port) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + try { + List rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("Error when querying flow rules", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + private Result checkEntityInternal(FlowRuleEntity entity) { + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null) { + return Result.ofFail(-1, "port can't be null"); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + if (entity.getGrade() == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (entity.getGrade() != 0 && entity.getGrade() != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + entity.getGrade() + " got"); + } + if (entity.getCount() == null || entity.getCount() < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + if (entity.getStrategy() == null) { + return Result.ofFail(-1, "strategy can't be null"); + } + if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) { + return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0"); + } + if (entity.getControlBehavior() == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + int controlBehavior = entity.getControlBehavior(); + if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null) { + return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1"); + } + if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null) { + return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2"); + } + if (entity.isClusterMode() && entity.getClusterConfig() == null) { + return Result.ofFail(-1, "cluster config should be valid"); + } + return null; + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddFlowRule(@RequestBody FlowRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(null); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + entity.setLimitApp(entity.getLimitApp().trim()); + entity.setResource(entity.getResource().trim()); + try { + entity = repository.save(entity); + + publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS); + return Result.ofSuccess(entity); + } + catch (Throwable t) { + Throwable e = t instanceof ExecutionException ? t.getCause() : t; + logger.error("Failed to add new flow rule, app={}, ip={}", entity.getApp(), entity.getIp(), e); + return Result.ofFail(-1, e.getMessage()); + } + } + + @PutMapping("/save.json") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateFlowRule(Long id, String app, String limitApp, String resource, + Integer grade, Double count, Integer strategy, String refResource, Integer controlBehavior, + Integer warmUpPeriodSec, Integer maxQueueingTimeMs) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + FlowRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "id " + id + " dose not exist"); + } + if (StringUtil.isNotBlank(app)) { + entity.setApp(app.trim()); + } + if (StringUtil.isNotBlank(limitApp)) { + entity.setLimitApp(limitApp.trim()); + } + if (StringUtil.isNotBlank(resource)) { + entity.setResource(resource.trim()); + } + if (grade != null) { + if (grade != 0 && grade != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got"); + } + entity.setGrade(grade); + } + if (count != null) { + entity.setCount(count); + } + if (strategy != null) { + if (strategy != 0 && strategy != 1 && strategy != 2) { + return Result.ofFail(-1, "strategy must be in [0, 1, 2], but " + strategy + " got"); + } + entity.setStrategy(strategy); + if (strategy != 0) { + if (StringUtil.isBlank(refResource)) { + return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0"); + } + entity.setRefResource(refResource.trim()); + } + } + if (controlBehavior != null) { + if (controlBehavior != 0 && controlBehavior != 1 && controlBehavior != 2) { + return Result.ofFail(-1, "controlBehavior must be in [0, 1, 2], but " + controlBehavior + " got"); + } + if (controlBehavior == 1 && warmUpPeriodSec == null) { + return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1"); + } + if (controlBehavior == 2 && maxQueueingTimeMs == null) { + return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2"); + } + entity.setControlBehavior(controlBehavior); + if (warmUpPeriodSec != null) { + entity.setWarmUpPeriodSec(warmUpPeriodSec); + } + if (maxQueueingTimeMs != null) { + entity.setMaxQueueingTimeMs(maxQueueingTimeMs); + } + } + Date date = new Date(); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + if (entity == null) { + return Result.ofFail(-1, "save entity fail: null"); + } + + publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS); + return Result.ofSuccess(entity); + } + catch (Throwable t) { + Throwable e = t instanceof ExecutionException ? t.getCause() : t; + logger.error("Error when updating flow rules, app={}, ip={}, ruleId={}", entity.getApp(), entity.getIp(), + id, e); + return Result.ofFail(-1, e.getMessage()); + } + } + + @DeleteMapping("/delete.json") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiDeleteFlowRule(Long id) { + + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + FlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } + catch (Exception e) { + return Result.ofFail(-1, e.getMessage()); + } + try { + publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get(5000, TimeUnit.MILLISECONDS); + return Result.ofSuccess(id); + } + catch (Throwable t) { + Throwable e = t instanceof ExecutionException ? t.getCause() : t; + logger.error("Error when deleting flow rules, app={}, ip={}, id={}", oldEntity.getApp(), oldEntity.getIp(), + id, e); + return Result.ofFail(-1, e.getMessage()); + } + } + + private CompletableFuture publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java new file mode 100644 index 0000000..887659b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java @@ -0,0 +1,80 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineDiscovery; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping(value = "/registry", produces = MediaType.APPLICATION_JSON_VALUE) +public class MachineRegistryController { + + private final Logger logger = LoggerFactory.getLogger(MachineRegistryController.class); + + @Autowired + private AppManagement appManagement; + + @ResponseBody + @RequestMapping("/machine") + public Result receiveHeartBeat(String app, + @RequestParam(value = "app_type", required = false, defaultValue = "0") Integer appType, Long version, + String v, String hostname, String ip, Integer port) { + if (app == null) { + app = MachineDiscovery.UNKNOWN_APP_NAME; + } + if (ip == null) { + return Result.ofFail(-1, "ip can't be null"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + if (port == -1) { + logger.info("Receive heartbeat from " + ip + " but port not set yet"); + return Result.ofFail(-1, "your port not set yet"); + } + String sentinelVersion = StringUtil.isEmpty(v) ? "unknown" : v; + version = version == null ? System.currentTimeMillis() : version; + try { + MachineInfo machineInfo = new MachineInfo(); + machineInfo.setApp(app); + machineInfo.setAppType(appType); + machineInfo.setHostname(hostname); + machineInfo.setIp(ip); + machineInfo.setPort(port); + machineInfo.setHeartbeatVersion(version); + machineInfo.setLastHeartbeat(System.currentTimeMillis()); + machineInfo.setVersion(sentinelVersion); + appManagement.addMachine(machineInfo); + return Result.ofSuccessMsg("success"); + } + catch (Exception e) { + logger.error("Receive heartbeat error", e); + return Result.ofFail(-1, e.getMessage()); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MetricController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MetricController.java new file mode 100644 index 0000000..9e4029a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MetricController.java @@ -0,0 +1,158 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.MetricVo; +import com.alibaba.csp.sentinel.dashboard.repository.metric.MetricsRepository; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author leyou + */ +@Controller +@RequestMapping(value = "/metric", produces = MediaType.APPLICATION_JSON_VALUE) +public class MetricController { + + private static Logger logger = LoggerFactory.getLogger(MetricController.class); + + private static final long maxQueryIntervalMs = 1000 * 60 * 60; + + @Autowired + private MetricsRepository metricStore; + + @ResponseBody + @RequestMapping("/queryTopResourceMetric.json") + public Result queryTopResourceMetric(final String app, Integer pageIndex, Integer pageSize, Boolean desc, + Long startTime, Long endTime, String searchKey) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (pageIndex == null || pageIndex <= 0) { + pageIndex = 1; + } + if (pageSize == null) { + pageSize = 6; + } + if (pageSize >= 20) { + pageSize = 20; + } + if (desc == null) { + desc = true; + } + if (null == endTime){ + endTime = System.currentTimeMillis(); + } + // 获取一个小时的数据 + if (null == startTime){ + startTime = endTime - maxQueryIntervalMs; + } + List resources = metricStore.listResourcesOfApp(app); + logger.debug("queryTopResourceMetric(), resources.size()={}", resources.size()); + + if (resources.isEmpty()) { + return Result.ofSuccess(null); + } + if (!desc) { + Collections.reverse(resources); + } + if (StringUtil.isNotEmpty(searchKey)) { + List searched = new ArrayList<>(); + for (String resource : resources) { + if (resource.contains(searchKey)) { + searched.add(resource); + } + } + resources = searched; + } + int totalPage = (resources.size() + pageSize - 1) / pageSize; + List topResource = new ArrayList<>(); + if (pageIndex <= totalPage) { + topResource = resources.subList((pageIndex - 1) * pageSize, + Math.min(pageIndex * pageSize, resources.size())); + } + final Map> map = new ConcurrentHashMap<>(); + logger.debug("topResource={}", topResource); + long time = System.currentTimeMillis(); + for (final String resource : topResource) { + List entities = metricStore.queryByAppAndResourceBetween(app, resource, startTime, endTime); + logger.debug("resource={}, entities.size()={}", resource, entities == null ? "null" : entities.size()); + List vos = MetricVo.fromMetricEntities(entities, resource); + Iterable vosSorted = sortMetricVoAndDistinct(vos); + map.put(resource, vosSorted); + } + logger.debug("queryTopResourceMetric() total query time={} ms", System.currentTimeMillis() - time); + Map resultMap = new HashMap<>(16); + resultMap.put("totalCount", resources.size()); + resultMap.put("totalPage", totalPage); + resultMap.put("pageIndex", pageIndex); + resultMap.put("pageSize", pageSize); + + Map> map2 = new LinkedHashMap<>(); + // order matters. + for (String identity : topResource) { + map2.put(identity, map.get(identity)); + } + resultMap.put("metric", map2); + return Result.ofSuccess(resultMap); + } + + @ResponseBody + @RequestMapping("/queryByAppAndResource.json") + public Result queryByAppAndResource(String app, String identity, Long startTime, Long endTime) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(identity)) { + return Result.ofFail(-1, "identity can't be null or empty"); + } + if (endTime == null) { + endTime = System.currentTimeMillis(); + } + if (startTime == null) { + startTime = endTime - maxQueryIntervalMs; + } + List entities = metricStore.queryByAppAndResourceBetween(app, identity, startTime, endTime); + List vos = MetricVo.fromMetricEntities(entities, identity); + return Result.ofSuccess(sortMetricVoAndDistinct(vos)); + } + + private Iterable sortMetricVoAndDistinct(List vos) { + if (vos == null) { + return null; + } + Map map = new TreeMap<>(); + for (MetricVo vo : vos) { + MetricVo oldVo = map.get(vo.getTimestamp()); + if (oldVo == null || vo.getGmtCreate() > oldVo.getGmtCreate()) { + map.put(vo.getTimestamp(), vo); + } + } + return map.values(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java new file mode 100644 index 0000000..4134d42 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java @@ -0,0 +1,271 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.CommandNotFoundException; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.dashboard.util.VersionUtils; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +@RestController +@RequestMapping(value = "/paramFlow") +public class ParamFlowRuleController { + + private final Logger logger = LoggerFactory.getLogger(ParamFlowRuleController.class); + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private AppManagement appManagement; + + @Autowired + private RuleRepository repository; + + private boolean checkIfSupported(String app, String ip, int port) { + try { + return Optional.ofNullable(appManagement.getDetailApp(app)).flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()).map(v -> v.greaterOrEqual(version020))) + .orElse(true); + // If error occurred or cannot retrieve machine info, return true. + } + catch (Exception ex) { + return true; + } + } + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, + @RequestParam String ip, @RequestParam Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip cannot be null or empty"); + } + if (port == null || port <= 0) { + return Result.ofFail(-1, "Invalid parameter: port"); + } + if (!checkIfSupported(app, ip, port)) { + return unsupportedVersion(); + } + try { + return sentinelApiClient.fetchParamFlowRulesOfMachine(app, ip, port).thenApply(repository::saveAll) + .thenApply(Result::ofSuccess).get(); + } + catch (ExecutionException ex) { + logger.error("Error when querying parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + catch (Throwable throwable) { + logger.error("Error when querying parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private boolean isNotSupported(Throwable ex) { + return ex instanceof CommandNotFoundException; + } + + @PostMapping("/rule") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result apiAddParamFlowRule(@RequestBody ParamFlowRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) { + return unsupportedVersion(); + } + entity.setId(null); + entity.getRule().setResource(entity.getResource().trim()); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(); + return Result.ofSuccess(entity); + } + catch (ExecutionException ex) { + logger.error("Error when adding new parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + catch (Throwable throwable) { + logger.error("Error when adding new parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private Result checkEntityInternal(ParamFlowRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "bad rule body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "port can't be null"); + } + if (entity.getRule() == null) { + return Result.ofFail(-1, "rule can't be null"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource name cannot be null or empty"); + } + if (entity.getCount() < 0) { + return Result.ofFail(-1, "count should be valid"); + } + if (entity.getGrade() != RuleConstant.FLOW_GRADE_QPS) { + return Result.ofFail(-1, "Unknown mode (blockGrade) for parameter flow control"); + } + if (entity.getParamIdx() == null || entity.getParamIdx() < 0) { + return Result.ofFail(-1, "paramIdx should be valid"); + } + if (entity.getDurationInSec() <= 0) { + return Result.ofFail(-1, "durationInSec should be valid"); + } + if (entity.getControlBehavior() < 0) { + return Result.ofFail(-1, "controlBehavior should be valid"); + } + return null; + } + + @PutMapping("/rule/{id}") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, + @RequestBody ParamFlowRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + ParamFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "id " + id + " does not exist"); + } + + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) { + return unsupportedVersion(); + } + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(); + return Result.ofSuccess(entity); + } + catch (ExecutionException ex) { + logger.error("Error when updating parameter flow rules, id=" + id, ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + catch (Throwable throwable) { + logger.error("Error when updating parameter flow rules, id=" + id, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id cannot be null"); + } + ParamFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get(); + return Result.ofSuccess(id); + } + catch (ExecutionException ex) { + logger.error("Error when deleting parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + catch (Throwable throwable) { + logger.error("Error when deleting parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private CompletableFuture publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.setParamFlowRuleOfMachine(app, ip, port, rules); + } + + private Result unsupportedVersion() { + return Result.ofFail(4041, + "Sentinel client not supported for parameter flow control (unsupported version or dependency absent)"); + } + + private final SentinelVersion version020 = new SentinelVersion().setMinorVersion(2); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ResourceController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ResourceController.java new file mode 100644 index 0000000..ad9148c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ResourceController.java @@ -0,0 +1,91 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.domain.ResourceTreeNode; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.ResourceVo; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Carpenter Lee + */ +@RestController +@RequestMapping(value = "/resource") +public class ResourceController { + + private static Logger logger = LoggerFactory.getLogger(ResourceController.class); + + @Autowired + private SentinelApiClient httpFetcher; + + /** + * Fetch real time statistics info of the machine. + * @param ip ip to fetch + * @param port port of the ip + * @param type one of [root, default, cluster], 'root' means fetching from tree root + * node, 'default' means fetching from tree default node, 'cluster' means fetching + * from cluster node. + * @param searchKey key to search + * @return node statistics info. + */ + @GetMapping("/machineResource.json") + public Result> fetchResourceChainListOfMachine(String ip, Integer port, String type, + String searchKey) { + if (StringUtil.isEmpty(ip) || port == null) { + return Result.ofFail(-1, "invalid param, give ip, port"); + } + final String ROOT = "root"; + final String DEFAULT = "default"; + if (StringUtil.isEmpty(type)) { + type = ROOT; + } + if (ROOT.equalsIgnoreCase(type) || DEFAULT.equalsIgnoreCase(type)) { + List nodeVos = httpFetcher.fetchResourceOfMachine(ip, port, type); + if (nodeVos == null) { + return Result.ofSuccess(null); + } + ResourceTreeNode treeNode = ResourceTreeNode.fromNodeVoList(nodeVos); + treeNode.searchIgnoreCase(searchKey); + return Result.ofSuccess(ResourceVo.fromResourceTreeNode(treeNode)); + } + else { + // Normal (cluster node). + List nodeVos = httpFetcher.fetchClusterNodeOfMachine(ip, port, true); + if (nodeVos == null) { + return Result.ofSuccess(null); + } + if (StringUtil.isNotEmpty(searchKey)) { + nodeVos = nodeVos.stream() + .filter(node -> node.getResource().toLowerCase().contains(searchKey.toLowerCase())) + .collect(Collectors.toList()); + } + return Result.ofSuccess(ResourceVo.fromNodeVoList(nodeVos)); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java new file mode 100644 index 0000000..58323f0 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java @@ -0,0 +1,257 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.SystemRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.List; + +/** + * @author leyou(lihao) + */ +@RestController +@RequestMapping("/system") +public class SystemController { + + private final Logger logger = LoggerFactory.getLogger(SystemController.class); + + @Autowired + private RuleRepository repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + private Result checkBasicParams(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + if (port <= 0 || port > 65535) { + return Result.ofFail(-1, "port should be in (0, 65535)"); + } + return null; + } + + @GetMapping("/rules.json") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(String app, String ip, Integer port) { + Result> checkResult = checkBasicParams(app, ip, port); + if (checkResult != null) { + return checkResult; + } + try { + List rules = sentinelApiClient.fetchSystemRuleOfMachine(app, ip, port); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("Query machine system rules error", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + private int countNotNullAndNotNegative(Number... values) { + int notNullCount = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] != null && values[i].doubleValue() >= 0) { + notNullCount++; + } + } + return notNullCount; + } + + @RequestMapping("/new.json") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAdd(String app, String ip, Integer port, Double highestSystemLoad, + Double highestCpuUsage, Long avgRt, Long maxThread, Double qps) { + + Result checkResult = checkBasicParams(app, ip, port); + if (checkResult != null) { + return checkResult; + } + + int notNullCount = countNotNullAndNotNegative(highestSystemLoad, avgRt, maxThread, qps, highestCpuUsage); + if (notNullCount != 1) { + return Result.ofFail(-1, "only one of [highestSystemLoad, avgRt, maxThread, qps,highestCpuUsage] " + + "value must be set > 0, but " + notNullCount + " values get"); + } + if (null != highestCpuUsage && highestCpuUsage > 1) { + return Result.ofFail(-1, "highestCpuUsage must between [0.0, 1.0]"); + } + SystemRuleEntity entity = new SystemRuleEntity(); + entity.setApp(app.trim()); + entity.setIp(ip.trim()); + entity.setPort(port); + // -1 is a fake value + if (null != highestSystemLoad) { + entity.setHighestSystemLoad(highestSystemLoad); + } + else { + entity.setHighestSystemLoad(-1D); + } + + if (null != highestCpuUsage) { + entity.setHighestCpuUsage(highestCpuUsage); + } + else { + entity.setHighestCpuUsage(-1D); + } + + if (avgRt != null) { + entity.setAvgRt(avgRt); + } + else { + entity.setAvgRt(-1L); + } + if (maxThread != null) { + entity.setMaxThread(maxThread); + } + else { + entity.setMaxThread(-1L); + } + if (qps != null) { + entity.setQps(qps); + } + else { + entity.setQps(-1D); + } + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } + catch (Throwable throwable) { + logger.error("Add SystemRule error", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(app, ip, port)) { + logger.warn("Publish system rules fail after rule add"); + } + return Result.ofSuccess(entity); + } + + @GetMapping("/save.json") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateIfNotNull(Long id, String app, Double highestSystemLoad, + Double highestCpuUsage, Long avgRt, Long maxThread, Double qps) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + SystemRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "id " + id + " dose not exist"); + } + + if (StringUtil.isNotBlank(app)) { + entity.setApp(app.trim()); + } + if (highestSystemLoad != null) { + if (highestSystemLoad < 0) { + return Result.ofFail(-1, "highestSystemLoad must >= 0"); + } + entity.setHighestSystemLoad(highestSystemLoad); + } + if (highestCpuUsage != null) { + if (highestCpuUsage < 0) { + return Result.ofFail(-1, "highestCpuUsage must >= 0"); + } + if (highestCpuUsage > 1) { + return Result.ofFail(-1, "highestCpuUsage must <= 1"); + } + entity.setHighestCpuUsage(highestCpuUsage); + } + if (avgRt != null) { + if (avgRt < 0) { + return Result.ofFail(-1, "avgRt must >= 0"); + } + entity.setAvgRt(avgRt); + } + if (maxThread != null) { + if (maxThread < 0) { + return Result.ofFail(-1, "maxThread must >= 0"); + } + entity.setMaxThread(maxThread); + } + if (qps != null) { + if (qps < 0) { + return Result.ofFail(-1, "qps must >= 0"); + } + entity.setQps(qps); + } + Date date = new Date(); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } + catch (Throwable throwable) { + logger.error("save error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.info("publish system rules fail after rule update"); + } + return Result.ofSuccess(entity); + } + + @RequestMapping("/delete.json") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result delete(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + SystemRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + } + catch (Throwable throwable) { + logger.error("delete error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.info("publish system rules fail after rule delete"); + } + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.setSystemRuleOfMachine(app, ip, port, rules); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/VersionController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/VersionController.java new file mode 100644 index 0000000..baff70d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/VersionController.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author hisenyuan + * @since 1.7.0 + */ +@RestController +public class VersionController { + + private static final String VERSION_PATTERN = "-"; + + @Value("${sentinel.dashboard.version}") + private String sentinelDashboardVersion; + + @GetMapping("/version") + public Result apiGetVersion() { + if (StringUtil.isNotBlank(sentinelDashboardVersion)) { + String res = sentinelDashboardVersion; + if (sentinelDashboardVersion.contains(VERSION_PATTERN)) { + res = sentinelDashboardVersion.substring(0, sentinelDashboardVersion.indexOf(VERSION_PATTERN)); + } + return Result.ofSuccess(res); + } + else { + return Result.ofFail(1, "getVersion failed: empty version"); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterAssignController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterAssignController.java new file mode 100644 index 0000000..42c306f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterAssignController.java @@ -0,0 +1,102 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller.cluster; + +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterAppAssignResultVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterAppFullAssignRequest; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterAppSingleServerAssignRequest; +import com.alibaba.csp.sentinel.dashboard.service.ClusterAssignService; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Collections; +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +@RestController +@RequestMapping("/cluster/assign") +public class ClusterAssignController { + + private final Logger logger = LoggerFactory.getLogger(ClusterAssignController.class); + + @Autowired + private ClusterAssignService clusterAssignService; + + @PostMapping("/all_server/{app}") + public Result apiAssignAllClusterServersOfApp(@PathVariable String app, + @RequestBody ClusterAppFullAssignRequest assignRequest) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (assignRequest == null || assignRequest.getClusterMap() == null + || assignRequest.getRemainingList() == null) { + return Result.ofFail(-1, "bad request body"); + } + try { + return Result.ofSuccess(clusterAssignService.applyAssignToApp(app, assignRequest.getClusterMap(), + assignRequest.getRemainingList())); + } + catch (Throwable throwable) { + logger.error("Error when assigning full cluster servers for app: " + app, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @PostMapping("/single_server/{app}") + public Result apiAssignSingleClusterServersOfApp(@PathVariable String app, + @RequestBody ClusterAppSingleServerAssignRequest assignRequest) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (assignRequest == null || assignRequest.getClusterMap() == null) { + return Result.ofFail(-1, "bad request body"); + } + try { + return Result.ofSuccess(clusterAssignService.applyAssignToApp(app, + Collections.singletonList(assignRequest.getClusterMap()), assignRequest.getRemainingList())); + } + catch (Throwable throwable) { + logger.error("Error when assigning single cluster servers for app: " + app, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @PostMapping("/unbind_server/{app}") + public Result apiUnbindClusterServersOfApp(@PathVariable String app, + @RequestBody Set machineIds) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (machineIds == null || machineIds.isEmpty()) { + return Result.ofFail(-1, "bad request body"); + } + try { + return Result.ofSuccess(clusterAssignService.unbindClusterServers(app, machineIds)); + } + catch (Throwable throwable) { + logger.error("Error when unbinding cluster server {} for app <{}>", machineIds, app, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterConfigController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterConfigController.java new file mode 100644 index 0000000..f55dae8 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/cluster/ClusterConfigController.java @@ -0,0 +1,241 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller.cluster; + +import com.alibaba.csp.sentinel.cluster.ClusterStateManager; +import com.alibaba.csp.sentinel.dashboard.client.CommandNotFoundException; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterClientModifyRequest; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterModifyRequest; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterServerModifyRequest; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.AppClusterClientStateWrapVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.AppClusterServerStateWrapVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterUniversalStatePairVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterUniversalStateVO; +import com.alibaba.csp.sentinel.dashboard.service.ClusterConfigService; +import com.alibaba.csp.sentinel.dashboard.util.ClusterEntityUtils; +import com.alibaba.csp.sentinel.dashboard.util.VersionUtils; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@RestController +@RequestMapping(value = "/cluster") +public class ClusterConfigController { + + private final Logger logger = LoggerFactory.getLogger(ClusterConfigController.class); + + private final SentinelVersion version140 = new SentinelVersion().setMajorVersion(1).setMinorVersion(4); + + @Autowired + private AppManagement appManagement; + + @Autowired + private ClusterConfigService clusterConfigService; + + @PostMapping("/config/modify_single") + public Result apiModifyClusterConfig(@RequestBody String payload) { + if (StringUtil.isBlank(payload)) { + return Result.ofFail(-1, "empty request body"); + } + try { + JSONObject body = JSON.parseObject(payload); + if (body.containsKey(KEY_MODE)) { + int mode = body.getInteger(KEY_MODE); + switch (mode) { + case ClusterStateManager.CLUSTER_CLIENT: + ClusterClientModifyRequest data = JSON.parseObject(payload, ClusterClientModifyRequest.class); + Result res = checkValidRequest(data); + if (res != null) { + return res; + } + clusterConfigService.modifyClusterClientConfig(data).get(); + return Result.ofSuccess(true); + case ClusterStateManager.CLUSTER_SERVER: + ClusterServerModifyRequest d = JSON.parseObject(payload, ClusterServerModifyRequest.class); + Result r = checkValidRequest(d); + if (r != null) { + return r; + } + // TODO: bad design here, should refactor! + clusterConfigService.modifyClusterServerConfig(d).get(); + return Result.ofSuccess(true); + default: + return Result.ofFail(-1, "invalid mode"); + } + } + return Result.ofFail(-1, "invalid parameter"); + } + catch (ExecutionException ex) { + logger.error("Error when modifying cluster config", ex.getCause()); + return errorResponse(ex); + } + catch (Throwable ex) { + logger.error("Error when modifying cluster config", ex); + return Result.ofFail(-1, ex.getMessage()); + } + } + + private Result errorResponse(ExecutionException ex) { + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + + @GetMapping("/state_single") + public Result apiGetClusterState(@RequestParam String app, @RequestParam String ip, + @RequestParam Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip cannot be null or empty"); + } + if (port == null || port <= 0) { + return Result.ofFail(-1, "Invalid parameter: port"); + } + if (!checkIfSupported(app, ip, port)) { + return unsupportedVersion(); + } + try { + return clusterConfigService.getClusterUniversalState(app, ip, port).thenApply(Result::ofSuccess).get(); + } + catch (ExecutionException ex) { + logger.error("Error when fetching cluster state", ex.getCause()); + return errorResponse(ex); + } + catch (Throwable throwable) { + logger.error("Error when fetching cluster state", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @GetMapping("/server_state/{app}") + public Result> apiGetClusterServerStateOfApp(@PathVariable String app) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + try { + return clusterConfigService.getClusterUniversalState(app) + .thenApply(ClusterEntityUtils::wrapToAppClusterServerState).thenApply(Result::ofSuccess).get(); + } + catch (ExecutionException ex) { + logger.error("Error when fetching cluster server state of app: " + app, ex.getCause()); + return errorResponse(ex); + } + catch (Throwable throwable) { + logger.error("Error when fetching cluster server state of app: " + app, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @GetMapping("/client_state/{app}") + public Result> apiGetClusterClientStateOfApp(@PathVariable String app) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + try { + return clusterConfigService.getClusterUniversalState(app) + .thenApply(ClusterEntityUtils::wrapToAppClusterClientState).thenApply(Result::ofSuccess).get(); + } + catch (ExecutionException ex) { + logger.error("Error when fetching cluster token client state of app: " + app, ex.getCause()); + return errorResponse(ex); + } + catch (Throwable throwable) { + logger.error("Error when fetching cluster token client state of app: " + app, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @GetMapping("/state/{app}") + public Result> apiGetClusterStateOfApp(@PathVariable String app) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + try { + return clusterConfigService.getClusterUniversalState(app).thenApply(Result::ofSuccess).get(); + } + catch (ExecutionException ex) { + logger.error("Error when fetching cluster state of app: " + app, ex.getCause()); + return errorResponse(ex); + } + catch (Throwable throwable) { + logger.error("Error when fetching cluster state of app: " + app, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private boolean isNotSupported(Throwable ex) { + return ex instanceof CommandNotFoundException; + } + + private boolean checkIfSupported(String app, String ip, int port) { + try { + return Optional.ofNullable(appManagement.getDetailApp(app)).flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()).map(v -> v.greaterOrEqual(version140))) + .orElse(true); + // If error occurred or cannot retrieve machine info, return true. + } + catch (Exception ex) { + return true; + } + } + + private Result checkValidRequest(ClusterModifyRequest request) { + if (StringUtil.isEmpty(request.getApp())) { + return Result.ofFail(-1, "app cannot be empty"); + } + if (StringUtil.isEmpty(request.getIp())) { + return Result.ofFail(-1, "ip cannot be empty"); + } + if (request.getPort() == null || request.getPort() < 0) { + return Result.ofFail(-1, "invalid port"); + } + if (request.getMode() == null || request.getMode() < 0) { + return Result.ofFail(-1, "invalid mode"); + } + if (!checkIfSupported(request.getApp(), request.getIp(), request.getPort())) { + return unsupportedVersion(); + } + return null; + } + + private Result unsupportedVersion() { + return Result.ofFail(4041, + "Sentinel client not supported for cluster flow control (unsupported version or dependency absent)"); + } + + private static final String KEY_MODE = "mode"; + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java new file mode 100644 index 0000000..32e1888 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java @@ -0,0 +1,267 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiPredicateItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.AddApiReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.ApiPredicateItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.UpdateApiReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemApiDefinitionStore; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; + +/** + * Gateway api Controller for manage gateway api definitions. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/api") +public class GatewayApiController { + + private final Logger logger = LoggerFactory.getLogger(GatewayApiController.class); + + @Autowired + private InMemApiDefinitionStore repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryApis(String app, String ip, Integer port) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List apis = sentinelApiClient.fetchApis(app, ip, port).get(); + repository.saveAll(apis); + return Result.ofSuccess(apis); + } + catch (Throwable throwable) { + logger.error("queryApis error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addApi(HttpServletRequest request, @RequestBody AddApiReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API名称 + String apiName = reqVo.getApiName(); + if (StringUtil.isBlank(apiName)) { + return Result.ofFail(-1, "apiName can't be null or empty"); + } + entity.setApiName(apiName.trim()); + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + Integer matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + // 检查API名称不能重复 + List allApis = repository.findAllByMachine(MachineInfo.of(app.trim(), ip.trim(), port)); + if (allApis.stream().map(o -> o.getApiName()).anyMatch(o -> o.equals(apiName.trim()))) { + return Result.ofFail(-1, "apiName exists: " + apiName); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } + catch (Throwable throwable) { + logger.error("add gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, ip, port)) { + logger.warn("publish gateway apis fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateApi(@RequestBody UpdateApiReqVo reqVo) { + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "api does not exist, id=" + id); + } + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + int matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "Invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } + catch (Throwable throwable) { + logger.error("update gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway apis fail after update"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + + public Result deleteApi(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } + catch (Throwable throwable) { + logger.error("delete gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway apis fail after delete"); + } + + return Result.ofSuccess(id); + } + + private boolean publishApis(String app, String ip, Integer port) { + List apis = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.modifyApis(app, ip, port, apis); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java new file mode 100644 index 0000000..016d4f3 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java @@ -0,0 +1,445 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayParamFlowItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.AddFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.GatewayParamFlowItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.UpdateFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemGatewayFlowRuleStore; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity.*; +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.*; + +/** + * Gateway flow rule Controller for manage gateway flow rules. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/flow") +public class GatewayFlowRuleController { + + private final Logger logger = LoggerFactory.getLogger(GatewayFlowRuleController.class); + + @Autowired + private InMemGatewayFlowRuleStore repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryFlowRules(String app, String ip, Integer port) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List rules = sentinelApiClient.fetchGatewayFlowRules(app, ip, port).get(); + repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("query gateway flow rules error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addFlowRule(@RequestBody AddFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API类型, Route ID或API分组 + Integer resourceMode = reqVo.getResourceMode(); + if (resourceMode == null) { + return Result.ofFail(-1, "resourceMode can't be null"); + } + if (!Arrays.asList(RESOURCE_MODE_ROUTE_ID, RESOURCE_MODE_CUSTOM_API_NAME).contains(resourceMode)) { + return Result.ofFail(-1, "invalid resourceMode: " + resourceMode); + } + entity.setResourceMode(resourceMode); + + // API名称 + String resource = reqVo.getResource(); + if (StringUtil.isBlank(resource)) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + entity.setResource(resource.trim()); + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER, + PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE) + .contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + } + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays + .asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY) + .contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } + else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 1-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } + catch (Throwable throwable) { + logger.error("add gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, ip, port)) { + logger.warn("publish gateway flow rules fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id); + } + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER, + PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE) + .contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + } + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays + .asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + else { + entity.setParamItem(null); + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY) + .contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } + else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 2-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } + catch (Throwable throwable) { + logger.error("update gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway flow rules fail after update"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + public Result deleteFlowRule(Long id) { + + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } + catch (Throwable throwable) { + logger.error("delete gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway flow rules fail after delete"); + } + + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.modifyGatewayFlowRules(app, ip, port, rules); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/AuthorityRuleControllerV2.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/AuthorityRuleControllerV2.java new file mode 100644 index 0000000..8df05a2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/AuthorityRuleControllerV2.java @@ -0,0 +1,186 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; + +/** + * @author Eric Zhao + * @since 0.2.1 + * 授权规则 + */ +@RestController +@RequestMapping(value = "/authority") +public class AuthorityRuleControllerV2 { + + private final Logger logger = LoggerFactory.getLogger(AuthorityRuleControllerV2.class); + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private RuleRepository repository; + + @Autowired + @Qualifier("authRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + + @Autowired + @Qualifier("authRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, + @RequestParam String ip, @RequestParam Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + try { + List rules = ruleProvider.getRules(app); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("Error when querying authority rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private Result checkEntityInternal(AuthorityRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "bad rule body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "port can't be null"); + } + if (entity.getRule() == null) { + return Result.ofFail(-1, "rule can't be null"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource name cannot be null or empty"); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp should be valid"); + } + if (entity.getStrategy() != RuleConstant.AUTHORITY_WHITE + && entity.getStrategy() != RuleConstant.AUTHORITY_BLACK) { + return Result.ofFail(-1, "Unknown strategy (must be blacklist or whitelist)"); + } + return null; + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddAuthorityRule(@RequestBody AuthorityRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(null); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + + publishRules(entity.getApp()); + } + catch (Throwable throwable) { + logger.error("Failed to add authority rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, + @RequestBody AuthorityRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(null); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + + publishRules(entity.getApp()); + } + catch (Throwable throwable) { + logger.error("Failed to save authority rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id cannot be null"); + } + AuthorityRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + + publishRules(oldEntity.getApp()); + } + catch (Exception e) { + return Result.ofFail(-1, e.getMessage()); + } + + return Result.ofSuccess(id); + } + + private void publishRules(String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/DegradeControllerV2.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/DegradeControllerV2.java new file mode 100644 index 0000000..e2edb84 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/DegradeControllerV2.java @@ -0,0 +1,220 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; + +/** + * Controller regarding APIs of degrade rules. Refactored since 1.8.0. + * + * @author Carpenter Lee + * @author Eric Zhao + */ +@RestController +@RequestMapping("/degrade") +public class DegradeControllerV2 { + + private final Logger logger = LoggerFactory.getLogger(DegradeControllerV2.class); + + @Autowired + private RuleRepository repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + @Qualifier("degradeNacosProvider") + private DynamicRuleProvider> ruleProvider; + + @Autowired + @Qualifier("degradeNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/rules.json") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + try { + List rules = ruleProvider.getRules(app); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("queryApps error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddRule(@RequestBody DegradeRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + + publishRules(entity.getApp()); + } + catch (Throwable t) { + logger.error("Failed to add new degrade rule, app={}, ip={}", entity.getApp(), entity.getIp(), t); + return Result.ofThrowable(-1, t); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateRule(@PathVariable("id") Long id, @RequestBody DegradeRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "id can't be null or negative"); + } + DegradeRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "Degrade rule does not exist, id=" + id); + } + entity.setApp(oldEntity.getApp()); + entity.setIp(oldEntity.getIp()); + entity.setPort(oldEntity.getPort()); + entity.setId(oldEntity.getId()); + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(new Date()); + try { + entity = repository.save(entity); + + publishRules(entity.getApp()); + } + catch (Throwable t) { + logger.error("Failed to save degrade rule, id={}, rule={}", id, entity, t); + return Result.ofThrowable(-1, t); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result delete(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + DegradeRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + + publishRules(oldEntity.getApp()); + } + catch (Throwable throwable) { + logger.error("Failed to delete degrade rule, id={}", id, throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(id); + } + + private void publishRules(String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + } + + private Result checkEntityInternal(DegradeRuleEntity entity) { + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be blank"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "invalid port: " + entity.getPort()); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + Double threshold = entity.getCount(); + if (threshold == null || threshold < 0) { + return Result.ofFail(-1, "invalid threshold: " + threshold); + } + Integer recoveryTimeoutSec = entity.getTimeWindow(); + if (recoveryTimeoutSec == null || recoveryTimeoutSec <= 0) { + return Result.ofFail(-1, "recoveryTimeout should be positive"); + } + Integer strategy = entity.getGrade(); + if (strategy == null) { + return Result.ofFail(-1, "circuit breaker strategy cannot be null"); + } + if (strategy < CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType() + || strategy > RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) { + return Result.ofFail(-1, "Invalid circuit breaker strategy: " + strategy); + } + if (entity.getMinRequestAmount() == null || entity.getMinRequestAmount() <= 0) { + return Result.ofFail(-1, "Invalid minRequestAmount"); + } + if (entity.getStatIntervalMs() == null || entity.getStatIntervalMs() <= 0) { + return Result.ofFail(-1, "Invalid statInterval"); + } + if (strategy == RuleConstant.DEGRADE_GRADE_RT) { + Double slowRatio = entity.getSlowRatioThreshold(); + if (slowRatio == null) { + return Result.ofFail(-1, "SlowRatioThreshold is required for slow request ratio strategy"); + } + else if (slowRatio < 0 || slowRatio > 1) { + return Result.ofFail(-1, "SlowRatioThreshold should be in range: [0.0, 1.0]"); + } + } + else if (strategy == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { + if (threshold > 1) { + return Result.ofFail(-1, "Ratio threshold should be in range: [0.0, 1.0]"); + } + } + return null; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java new file mode 100644 index 0000000..e14df50 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java @@ -0,0 +1,222 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; + +/** + * Flow rule controller (v2). + * + * @author Eric Zhao + * @since 1.4.0 + */ +@RestController +@RequestMapping(value = "/flow") +public class FlowControllerV2 { + + private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class); + + @Autowired + private InMemoryRuleRepositoryAdapter repository; + + @Autowired + @Qualifier("flowRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + + @Autowired + @Qualifier("flowRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(@RequestParam String app) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + try { + List rules = ruleProvider.getRules(app); + if (rules != null && !rules.isEmpty()) { + for (FlowRuleEntity entity : rules) { + entity.setApp(app); + if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null) { + entity.setId(entity.getClusterConfig().getFlowId()); + } + } + } + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("Error when querying flow rules", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + private Result checkEntityInternal(FlowRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "invalid body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + if (entity.getGrade() == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (entity.getGrade() != 0 && entity.getGrade() != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + entity.getGrade() + " got"); + } + if (entity.getCount() == null || entity.getCount() < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + if (entity.getStrategy() == null) { + return Result.ofFail(-1, "strategy can't be null"); + } + if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) { + return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0"); + } + if (entity.getControlBehavior() == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + int controlBehavior = entity.getControlBehavior(); + if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null) { + return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1"); + } + if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null) { + return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2"); + } + if (entity.isClusterMode() && entity.getClusterConfig() == null) { + return Result.ofFail(-1, "cluster config should be valid"); + } + return null; + } + + @PostMapping("/rule") + @AuthAction(value = AuthService.PrivilegeType.WRITE_RULE) + public Result apiAddFlowRule(@RequestBody FlowRuleEntity entity) { + + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(null); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + entity.setLimitApp(entity.getLimitApp().trim()); + entity.setResource(entity.getResource().trim()); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + } + catch (Throwable throwable) { + logger.error("Failed to add flow rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result apiUpdateFlowRule(@PathVariable("id") Long id, @RequestBody FlowRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + FlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "id " + id + " does not exist"); + } + if (entity == null) { + return Result.ofFail(-1, "invalid body"); + } + + entity.setApp(oldEntity.getApp()); + entity.setIp(oldEntity.getIp()); + entity.setPort(oldEntity.getPort()); + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + if (entity == null) { + return Result.ofFail(-1, "save entity fail"); + } + // 修改 + publishRules(oldEntity.getApp()); + } + catch (Throwable throwable) { + logger.error("Failed to update flow rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + FlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + // 删除配置 + publishRules(oldEntity.getApp()); + } + catch (Exception e) { + return Result.ofFail(-1, e.getMessage()); + } + return Result.ofSuccess(id); + } + + private void publishRules(/* @NonNull */ String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayApiControllerV2.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayApiControllerV2.java new file mode 100644 index 0000000..9cad9c6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayApiControllerV2.java @@ -0,0 +1,256 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiPredicateItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.AddApiReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.ApiPredicateItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.UpdateApiReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemApiDefinitionStore; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; + +/** + * Gateway api Controller for manage gateway api definitions. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/api") +public class GatewayApiControllerV2 { + + private final Logger logger = LoggerFactory.getLogger(GatewayApiControllerV2.class); + + @Autowired + private InMemApiDefinitionStore repository; + + @Autowired + @Qualifier("gatewayApiNacosProvider") + private DynamicRuleProvider> ruleProvider; + + @Autowired + @Qualifier("gatewayApiNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryApis(String app) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + try { + List apis = ruleProvider.getRules(app); + + repository.saveAll(apis); + return Result.ofSuccess(apis); + } + catch (Throwable throwable) { + logger.error("queryApis error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addApi(@RequestBody AddApiReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API名称 + String apiName = reqVo.getApiName(); + if (StringUtil.isBlank(apiName)) { + return Result.ofFail(-1, "apiName can't be null or empty"); + } + entity.setApiName(apiName.trim()); + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + Integer matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + // 检查API名称不能重复 + List allApis = repository.findAllByMachine(MachineInfo.of(app.trim(), ip.trim(), port)); + if (allApis.stream().map(ApiDefinitionEntity::getApiName).anyMatch(o -> o.equals(apiName.trim()))) { + return Result.ofFail(-1, "apiName exists: " + apiName); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + publishApis(entity.getApp()); + } + catch (Throwable throwable) { + logger.error("add gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateApi(@RequestBody UpdateApiReqVo reqVo) { + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "api does not exist, id=" + id); + } + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + int matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "Invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + + publishApis(entity.getApp()); + } + catch (Throwable throwable) { + logger.error("update gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + public Result deleteApi(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + + publishApis(oldEntity.getApp()); + } + catch (Throwable throwable) { + logger.error("delete gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + return Result.ofSuccess(id); + } + + private void publishApis(String app) throws Exception { + List apis = repository.findAllByApp(app); + rulePublisher.publish(app,apis); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayFlowRuleControllerV2.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayFlowRuleControllerV2.java new file mode 100644 index 0000000..0098900 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/GatewayFlowRuleControllerV2.java @@ -0,0 +1,439 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayParamFlowItemEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.AddFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.GatewayParamFlowItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.UpdateFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemGatewayFlowRuleStore; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity.*; +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.*; + +/** + * Gateway flow rule Controller for manage gateway flow rules. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/flow") +public class GatewayFlowRuleControllerV2 { + + private final Logger logger = LoggerFactory.getLogger(GatewayFlowRuleControllerV2.class); + + @Autowired + private InMemGatewayFlowRuleStore repository; + + @Autowired + @Qualifier("gatewayGlowRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + + @Autowired + @Qualifier("gatewayGlowRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryFlowRules(String app, String ip, Integer port) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + try { + List rules = ruleProvider.getRules(app); + + repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("query gateway flow rules error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addFlowRule(@RequestBody AddFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API类型, Route ID或API分组 + Integer resourceMode = reqVo.getResourceMode(); + if (resourceMode == null) { + return Result.ofFail(-1, "resourceMode can't be null"); + } + if (!Arrays.asList(RESOURCE_MODE_ROUTE_ID, RESOURCE_MODE_CUSTOM_API_NAME).contains(resourceMode)) { + return Result.ofFail(-1, "invalid resourceMode: " + resourceMode); + } + entity.setResourceMode(resourceMode); + + // API名称 + String resource = reqVo.getResource(); + if (StringUtil.isBlank(resource)) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + entity.setResource(resource.trim()); + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER, + PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE) + .contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + } + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays + .asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY) + .contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } + else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 1-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + + publishRules(entity.getApp()); + } + catch (Throwable throwable) { + logger.error("add gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id); + } + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER, + PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE) + .contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + } + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays + .asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX) + .contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + else { + entity.setParamItem(null); + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY) + .contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } + else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 2-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + + publishRules(entity.getApp()); + } + catch (Throwable throwable) { + logger.error("update gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + public Result deleteFlowRule(Long id) { + + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + + publishRules(oldEntity.getApp()); + } + catch (Throwable throwable) { + logger.error("delete gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + return Result.ofSuccess(id); + } + + private void publishRules(String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/ParamFlowRuleControllerV2.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/ParamFlowRuleControllerV2.java new file mode 100644 index 0000000..ee49a15 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/ParamFlowRuleControllerV2.java @@ -0,0 +1,267 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.CommandNotFoundException; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.dashboard.util.VersionUtils; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +/** + * @author Eric Zhao + * @since 0.2.1 + * 热点 + */ +@RestController +@RequestMapping(value = "/paramFlow") +public class ParamFlowRuleControllerV2 { + + private final Logger logger = LoggerFactory.getLogger(ParamFlowRuleControllerV2.class); + @Autowired + private AppManagement appManagement; + + @Autowired + private RuleRepository repository; + + @Autowired + @Qualifier("paramFlowRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + + @Autowired + @Qualifier("paramFlowRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + private boolean checkIfSupported(String app, String ip, int port) { + try { + return Optional.ofNullable(appManagement.getDetailApp(app)).flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()).map(v -> v.greaterOrEqual(version020))) + .orElse(true); + // If error occurred or cannot retrieve machine info, return true. + } + catch (Exception ex) { + return true; + } + } + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, + @RequestParam String ip, @RequestParam Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + try { + List rules = ruleProvider.getRules(app); + if (rules != null && !rules.isEmpty()) { + for (ParamFlowRuleEntity entity : rules) { + entity.setApp(app); + if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null) { + entity.setId(entity.getClusterConfig().getFlowId()); + } + } + } + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } + catch (Throwable throwable) { + logger.error("Error when querying flow rules", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + private boolean isNotSupported(Throwable ex) { + return ex instanceof CommandNotFoundException; + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddParamFlowRule(@RequestBody ParamFlowRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) { + return unsupportedVersion(); + } + entity.setId(null); + entity.getRule().setResource(entity.getResource().trim()); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + return Result.ofSuccess(entity); + } + catch (ExecutionException ex) { + logger.error("Error when adding new parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + catch (Throwable throwable) { + logger.error("Error when adding new parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private Result checkEntityInternal(ParamFlowRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "bad rule body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "port can't be null"); + } + if (entity.getRule() == null) { + return Result.ofFail(-1, "rule can't be null"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource name cannot be null or empty"); + } + if (entity.getCount() < 0) { + return Result.ofFail(-1, "count should be valid"); + } + if (entity.getGrade() != RuleConstant.FLOW_GRADE_QPS) { + return Result.ofFail(-1, "Unknown mode (blockGrade) for parameter flow control"); + } + if (entity.getParamIdx() == null || entity.getParamIdx() < 0) { + return Result.ofFail(-1, "paramIdx should be valid"); + } + if (entity.getDurationInSec() <= 0) { + return Result.ofFail(-1, "durationInSec should be valid"); + } + if (entity.getControlBehavior() < 0) { + return Result.ofFail(-1, "controlBehavior should be valid"); + } + return null; + } + + @PutMapping("/rule/{id}") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, + @RequestBody ParamFlowRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + ParamFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "id " + id + " does not exist"); + } + + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) { + return unsupportedVersion(); + } + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + return Result.ofSuccess(entity); + } + catch (ExecutionException ex) { + logger.error("Error when updating parameter flow rules, id=" + id, ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + catch (Throwable throwable) { + logger.error("Error when updating parameter flow rules, id=" + id, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id cannot be null"); + } + ParamFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + // 删除 + publishRules(oldEntity.getApp()); + return Result.ofSuccess(id); + } + catch (ExecutionException ex) { + logger.error("Error when deleting parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } + else { + return Result.ofThrowable(-1, ex.getCause()); + } + } + catch (Throwable throwable) { + logger.error("Error when deleting parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private void publishRules(String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + } + + private Result unsupportedVersion() { + return Result.ofFail(4041, + "Sentinel client not supported for parameter flow control (unsupported version or dependency absent)"); + } + + private final SentinelVersion version020 = new SentinelVersion().setMinorVersion(2); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java new file mode 100644 index 0000000..953c784 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java @@ -0,0 +1,108 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity; + +import com.alibaba.csp.sentinel.dashboard.discovery.AppInfo; + +import java.util.Date; + +/** + * @author leyou + */ +public class ApplicationEntity { + + private Long id; + + private Date gmtCreate; + + private Date gmtModified; + + private String app; + + private Integer appType; + + private String activeConsole; + + private Date lastFetch; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + + public String getActiveConsole() { + return activeConsole; + } + + public Date getLastFetch() { + return lastFetch; + } + + public void setLastFetch(Date lastFetch) { + this.lastFetch = lastFetch; + } + + public void setActiveConsole(String activeConsole) { + this.activeConsole = activeConsole; + } + + public AppInfo toAppInfo() { + return new AppInfo(app, appType); + } + + @Override + public String toString() { + return "ApplicationEntity{" + "id=" + id + ", gmtCreate=" + gmtCreate + ", gmtModified=" + gmtModified + + ", app='" + app + '\'' + ", activeConsole='" + activeConsole + '\'' + ", lastFetch=" + lastFetch + + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MachineEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MachineEntity.java new file mode 100644 index 0000000..79d8a7f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MachineEntity.java @@ -0,0 +1,127 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity; + +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; + +import java.util.Date; + +/** + * @author leyou + */ +public class MachineEntity { + + private Long id; + + private Date gmtCreate; + + private Date gmtModified; + + private String app; + + private String ip; + + private String hostname; + + private Date timestamp; + + private Integer port; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public MachineInfo toMachineInfo() { + MachineInfo machineInfo = new MachineInfo(); + + machineInfo.setApp(app); + machineInfo.setHostname(hostname); + machineInfo.setIp(ip); + machineInfo.setPort(port); + machineInfo.setLastHeartbeat(timestamp.getTime()); + machineInfo.setHeartbeatVersion(timestamp.getTime()); + + return machineInfo; + } + + @Override + public String toString() { + return "MachineEntity{" + "id=" + id + ", gmtCreate=" + gmtCreate + ", gmtModified=" + gmtModified + ", app='" + + app + '\'' + ", ip='" + ip + '\'' + ", hostname='" + hostname + '\'' + ", timestamp=" + timestamp + + ", port=" + port + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricEntity.java new file mode 100644 index 0000000..4dc9256 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricEntity.java @@ -0,0 +1,241 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity; + +import com.chushang.common.mongo.annotation.MongoDel; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.MongoId; + +import java.io.Serializable; + +/** + * @author leyou + */ +@Document(value = "MetricEntity") +@MongoDel +public class MetricEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @MongoId + private String mongoId; + + private Long id; + /** + * 时间戳 + */ + private Long gmtCreate; + + private Long gmtModified; + /** + * 应用名称 + */ + private String app; + + /** + * 监控信息的时间戳 + */ + private Long timestamp; + /** + * 资源名称 + */ + private String resource; + /** + * 通过qps + */ + private Long passQps; + /** + * 成功qps + */ + private Long successQps; + /** + * 限流qps + */ + private Long blockQps; + /** + * 异常qps + */ + private Long exceptionQps; + + /** + * summary rt of all success exit qps. + */ + private double rt; + + /** + * 本次聚合的总条数 + */ + private int count; + + private int resourceCode; + + public static MetricEntity copyOf(MetricEntity oldEntity) { + MetricEntity entity = new MetricEntity(); + entity.setId(oldEntity.getId()); + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(oldEntity.getGmtModified()); + entity.setApp(oldEntity.getApp()); + entity.setTimestamp(oldEntity.getTimestamp()); + entity.setResource(oldEntity.getResource()); + entity.setPassQps(oldEntity.getPassQps()); + entity.setBlockQps(oldEntity.getBlockQps()); + entity.setSuccessQps(oldEntity.getSuccessQps()); + entity.setExceptionQps(oldEntity.getExceptionQps()); + entity.setRt(oldEntity.getRt()); + entity.setCount(oldEntity.getCount()); + return entity; + } + + public synchronized void addPassQps(Long passQps) { + this.passQps += passQps; + } + + public synchronized void addBlockQps(Long blockQps) { + this.blockQps += blockQps; + } + + public synchronized void addExceptionQps(Long exceptionQps) { + this.exceptionQps += exceptionQps; + } + + public synchronized void addCount(int count) { + this.count += count; + } + + public synchronized void addRtAndSuccessQps(double avgRt, Long successQps) { + this.rt += avgRt * successQps; + this.successQps += successQps; + } + + /** + * {@link #rt} = {@code avgRt * successQps} + * @param avgRt average rt of {@code successQps} + * @param successQps + */ + public synchronized void setRtAndSuccessQps(double avgRt, Long successQps) { + this.rt = avgRt * successQps; + this.successQps = successQps; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Long gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Long getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Long gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + this.resourceCode = resource.hashCode(); + } + + public Long getPassQps() { + return passQps; + } + + public void setPassQps(Long passQps) { + this.passQps = passQps; + } + + public Long getBlockQps() { + return blockQps; + } + + public void setBlockQps(Long blockQps) { + this.blockQps = blockQps; + } + + public Long getExceptionQps() { + return exceptionQps; + } + + public void setExceptionQps(Long exceptionQps) { + this.exceptionQps = exceptionQps; + } + + public double getRt() { + return rt; + } + + public void setRt(double rt) { + this.rt = rt; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public int getResourceCode() { + return resourceCode; + } + + public Long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(Long successQps) { + this.successQps = successQps; + } + + + public String getMongoId() { + return mongoId; + } + + public void setMongoId(String mongoId) { + this.mongoId = mongoId; + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java new file mode 100644 index 0000000..b01b68a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java @@ -0,0 +1,121 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +/** + * @author leyou + */ +public class MetricPositionEntity { + + private long id; + + private Date gmtCreate; + + private Date gmtModified; + + private String app; + + private String ip; + + /** + * Sentinel在该应用上使用的端口 + */ + private int port; + + /** + * 机器名,冗余字段 + */ + private String hostname; + + /** + * 上一次拉取的最晚时间戳 + */ + private Date lastFetch; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public Date getLastFetch() { + return lastFetch; + } + + public void setLastFetch(Date lastFetch) { + this.lastFetch = lastFetch; + } + + @Override + public String toString() { + return "MetricPositionEntity{" + "id=" + id + ", gmtCreate=" + gmtCreate + ", gmtModified=" + gmtModified + + ", app='" + app + '\'' + ", ip='" + ip + '\'' + ", port=" + port + ", hostname='" + hostname + '\'' + + ", lastFetch=" + lastFetch + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/SentinelVersion.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/SentinelVersion.java new file mode 100644 index 0000000..7dddb8f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/SentinelVersion.java @@ -0,0 +1,136 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +public class SentinelVersion { + + private int majorVersion; + + private int minorVersion; + + private int fixVersion; + + private String postfix; + + public SentinelVersion() { + this(0, 0, 0); + } + + public SentinelVersion(int major, int minor, int fix) { + this(major, minor, fix, null); + } + + public SentinelVersion(int major, int minor, int fix, String postfix) { + this.majorVersion = major; + this.minorVersion = minor; + this.fixVersion = fix; + this.postfix = postfix; + } + + /** + * 000, 000, 000 + */ + public int getFullVersion() { + return majorVersion * 1000000 + minorVersion * 1000 + fixVersion; + } + + public int getMajorVersion() { + return majorVersion; + } + + public SentinelVersion setMajorVersion(int majorVersion) { + this.majorVersion = majorVersion; + return this; + } + + public int getMinorVersion() { + return minorVersion; + } + + public SentinelVersion setMinorVersion(int minorVersion) { + this.minorVersion = minorVersion; + return this; + } + + public int getFixVersion() { + return fixVersion; + } + + public SentinelVersion setFixVersion(int fixVersion) { + this.fixVersion = fixVersion; + return this; + } + + public String getPostfix() { + return postfix; + } + + public SentinelVersion setPostfix(String postfix) { + this.postfix = postfix; + return this; + } + + public boolean greaterThan(SentinelVersion version) { + if (version == null) { + return true; + } + return getFullVersion() > version.getFullVersion(); + } + + public boolean greaterOrEqual(SentinelVersion version) { + if (version == null) { + return true; + } + return getFullVersion() >= version.getFullVersion(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SentinelVersion that = (SentinelVersion) o; + + if (getFullVersion() != that.getFullVersion()) { + return false; + } + return postfix != null ? postfix.equals(that.postfix) : that.postfix == null; + } + + @Override + public int hashCode() { + int result = majorVersion; + result = 31 * result + minorVersion; + result = 31 * result + fixVersion; + result = 31 * result + (postfix != null ? postfix.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SentinelVersion{" + "majorVersion=" + majorVersion + ", minorVersion=" + minorVersion + ", fixVersion=" + + fixVersion + ", postfix='" + postfix + '\'' + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java new file mode 100644 index 0000000..dde2b19 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java @@ -0,0 +1,208 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; + +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Entity for {@link ApiDefinition}. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiDefinitionEntity implements RuleEntity { + + private Long id; + + private String app; + + private String ip; + + private Integer port; + + private Date gmtCreate; + + private Date gmtModified; + + private String apiName; + + private Set predicateItems; + + public static ApiDefinitionEntity fromApiDefinition(String app, String ip, Integer port, + ApiDefinition apiDefinition) { + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setApiName(apiDefinition.getApiName()); + + Set predicateItems = new LinkedHashSet<>(); + entity.setPredicateItems(predicateItems); + + Set apiPredicateItems = apiDefinition.getPredicateItems(); + if (apiPredicateItems != null) { + for (ApiPredicateItem apiPredicateItem : apiPredicateItems) { + ApiPredicateItemEntity itemEntity = new ApiPredicateItemEntity(); + predicateItems.add(itemEntity); + ApiPathPredicateItem pathPredicateItem = (ApiPathPredicateItem) apiPredicateItem; + itemEntity.setPattern(pathPredicateItem.getPattern()); + itemEntity.setMatchStrategy(pathPredicateItem.getMatchStrategy()); + } + } + + return entity; + } + + public ApiDefinition toApiDefinition() { + ApiDefinition apiDefinition = new ApiDefinition(); + apiDefinition.setApiName(apiName); + + Set apiPredicateItems = new LinkedHashSet<>(); + apiDefinition.setPredicateItems(apiPredicateItems); + + if (predicateItems != null) { + for (ApiPredicateItemEntity predicateItem : predicateItems) { + ApiPathPredicateItem apiPredicateItem = new ApiPathPredicateItem(); + apiPredicateItems.add(apiPredicateItem); + apiPredicateItem.setMatchStrategy(predicateItem.getMatchStrategy()); + apiPredicateItem.setPattern(predicateItem.getPattern()); + } + } + + return apiDefinition; + } + + public ApiDefinitionEntity() { + + } + + public ApiDefinitionEntity(String apiName, Set predicateItems) { + this.apiName = apiName; + this.predicateItems = predicateItems; + } + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public Set getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(Set predicateItems) { + this.predicateItems = predicateItems; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + @Override + public Rule toRule() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApiDefinitionEntity entity = (ApiDefinitionEntity) o; + return Objects.equals(id, entity.id) && Objects.equals(app, entity.app) && Objects.equals(ip, entity.ip) + && Objects.equals(port, entity.port) && Objects.equals(gmtCreate, entity.gmtCreate) + && Objects.equals(gmtModified, entity.gmtModified) && Objects.equals(apiName, entity.apiName) + && Objects.equals(predicateItems, entity.predicateItems); + } + + @Override + public int hashCode() { + return Objects.hash(id, app, ip, port, gmtCreate, gmtModified, apiName, predicateItems); + } + + @Override + public String toString() { + return "ApiDefinitionEntity{" + "id=" + id + ", app='" + app + '\'' + ", ip='" + ip + '\'' + ", port=" + port + + ", gmtCreate=" + gmtCreate + ", gmtModified=" + gmtModified + ", apiName='" + apiName + '\'' + + ", predicateItems=" + predicateItems + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java new file mode 100644 index 0000000..62a93c3 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java @@ -0,0 +1,80 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; + +import java.util.Objects; + +/** + * Entity for {@link ApiPredicateItem}. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiPredicateItemEntity { + + private String pattern; + + private Integer matchStrategy; + + public ApiPredicateItemEntity() { + } + + public ApiPredicateItemEntity(String pattern, int matchStrategy) { + this.pattern = pattern; + this.matchStrategy = matchStrategy; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApiPredicateItemEntity that = (ApiPredicateItemEntity) o; + return Objects.equals(pattern, that.pattern) && Objects.equals(matchStrategy, that.matchStrategy); + } + + @Override + public int hashCode() { + return Objects.hash(pattern, matchStrategy); + } + + @Override + public String toString() { + return "ApiPredicateItemEntity{" + "pattern='" + pattern + '\'' + ", matchStrategy=" + matchStrategy + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java new file mode 100644 index 0000000..a66a4fa --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java @@ -0,0 +1,352 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; + +import java.util.Date; +import java.util.Objects; + +/** + * Entity for {@link GatewayFlowRule}. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayFlowRuleEntity implements RuleEntity { + + /** 间隔单位 */ + /** 0-秒 */ + public static final int INTERVAL_UNIT_SECOND = 0; + + /** 1-分 */ + public static final int INTERVAL_UNIT_MINUTE = 1; + + /** 2-时 */ + public static final int INTERVAL_UNIT_HOUR = 2; + + /** 3-天 */ + public static final int INTERVAL_UNIT_DAY = 3; + + private Long id; + + private String app; + + private String ip; + + private Integer port; + + private Date gmtCreate; + + private Date gmtModified; + + private String resource; + + private Integer resourceMode; + + private Integer grade; + + private Double count; + + private Long interval; + + private Integer intervalUnit; + + private Integer controlBehavior; + + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemEntity paramItem; + + public static Long calIntervalSec(Long interval, Integer intervalUnit) { + switch (intervalUnit) { + case INTERVAL_UNIT_SECOND: + return interval; + case INTERVAL_UNIT_MINUTE: + return interval * 60; + case INTERVAL_UNIT_HOUR: + return interval * 60 * 60; + case INTERVAL_UNIT_DAY: + return interval * 60 * 60 * 24; + default: + break; + } + + throw new IllegalArgumentException("Invalid intervalUnit: " + intervalUnit); + } + + public static Object[] parseIntervalSec(Long intervalSec) { + if (intervalSec % (60 * 60 * 24) == 0) { + return new Object[] { intervalSec / (60 * 60 * 24), INTERVAL_UNIT_DAY }; + } + + if (intervalSec % (60 * 60) == 0) { + return new Object[] { intervalSec / (60 * 60), INTERVAL_UNIT_HOUR }; + } + + if (intervalSec % 60 == 0) { + return new Object[] { intervalSec / 60, INTERVAL_UNIT_MINUTE }; + } + + return new Object[] { intervalSec, INTERVAL_UNIT_SECOND }; + } + + public GatewayFlowRule toGatewayFlowRule() { + GatewayFlowRule rule = new GatewayFlowRule(); + rule.setResource(resource); + rule.setResourceMode(resourceMode); + + rule.setGrade(grade); + rule.setCount(count); + rule.setIntervalSec(calIntervalSec(interval, intervalUnit)); + + rule.setControlBehavior(controlBehavior); + + if (burst != null) { + rule.setBurst(burst); + } + + if (maxQueueingTimeoutMs != null) { + rule.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + if (paramItem != null) { + GatewayParamFlowItem ruleItem = new GatewayParamFlowItem(); + rule.setParamItem(ruleItem); + ruleItem.setParseStrategy(paramItem.getParseStrategy()); + ruleItem.setFieldName(paramItem.getFieldName()); + ruleItem.setPattern(paramItem.getPattern()); + + if (paramItem.getMatchStrategy() != null) { + ruleItem.setMatchStrategy(paramItem.getMatchStrategy()); + } + } + + return rule; + } + + public static GatewayFlowRuleEntity fromGatewayFlowRule(String app, String ip, Integer port, GatewayFlowRule rule) { + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + + entity.setResource(rule.getResource()); + entity.setResourceMode(rule.getResourceMode()); + + entity.setGrade(rule.getGrade()); + entity.setCount(rule.getCount()); + Object[] intervalSecResult = parseIntervalSec(rule.getIntervalSec()); + entity.setInterval((Long) intervalSecResult[0]); + entity.setIntervalUnit((Integer) intervalSecResult[1]); + + entity.setControlBehavior(rule.getControlBehavior()); + entity.setBurst(rule.getBurst()); + entity.setMaxQueueingTimeoutMs(rule.getMaxQueueingTimeoutMs()); + + GatewayParamFlowItem paramItem = rule.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + itemEntity.setFieldName(paramItem.getFieldName()); + itemEntity.setPattern(paramItem.getPattern()); + itemEntity.setMatchStrategy(paramItem.getMatchStrategy()); + } + + return entity; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + @Override + public Rule toRule() { + return null; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public GatewayParamFlowItemEntity getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemEntity paramItem) { + this.paramItem = paramItem; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getResourceMode() { + return resourceMode; + } + + public void setResourceMode(Integer resourceMode) { + this.resourceMode = resourceMode; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GatewayFlowRuleEntity that = (GatewayFlowRuleEntity) o; + return Objects.equals(id, that.id) && Objects.equals(app, that.app) && Objects.equals(ip, that.ip) + && Objects.equals(port, that.port) && Objects.equals(gmtCreate, that.gmtCreate) + && Objects.equals(gmtModified, that.gmtModified) && Objects.equals(resource, that.resource) + && Objects.equals(resourceMode, that.resourceMode) && Objects.equals(grade, that.grade) + && Objects.equals(count, that.count) && Objects.equals(interval, that.interval) + && Objects.equals(intervalUnit, that.intervalUnit) + && Objects.equals(controlBehavior, that.controlBehavior) && Objects.equals(burst, that.burst) + && Objects.equals(maxQueueingTimeoutMs, that.maxQueueingTimeoutMs) + && Objects.equals(paramItem, that.paramItem); + } + + @Override + public int hashCode() { + return Objects.hash(id, app, ip, port, gmtCreate, gmtModified, resource, resourceMode, grade, count, interval, + intervalUnit, controlBehavior, burst, maxQueueingTimeoutMs, paramItem); + } + + @Override + public String toString() { + return "GatewayFlowRuleEntity{" + "id=" + id + ", app='" + app + '\'' + ", ip='" + ip + '\'' + ", port=" + port + + ", gmtCreate=" + gmtCreate + ", gmtModified=" + gmtModified + ", resource='" + resource + '\'' + + ", resourceMode=" + resourceMode + ", grade=" + grade + ", count=" + count + ", interval=" + interval + + ", intervalUnit=" + intervalUnit + ", controlBehavior=" + controlBehavior + ", burst=" + burst + + ", maxQueueingTimeoutMs=" + maxQueueingTimeoutMs + ", paramItem=" + paramItem + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java new file mode 100644 index 0000000..2f36601 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java @@ -0,0 +1,94 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; + +import java.util.Objects; + +/** + * Entity for {@link GatewayParamFlowItem}. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayParamFlowItemEntity { + + private Integer parseStrategy; + + private String fieldName; + + private String pattern; + + private Integer matchStrategy; + + public Integer getParseStrategy() { + return parseStrategy; + } + + public void setParseStrategy(Integer parseStrategy) { + this.parseStrategy = parseStrategy; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GatewayParamFlowItemEntity that = (GatewayParamFlowItemEntity) o; + return Objects.equals(parseStrategy, that.parseStrategy) && Objects.equals(fieldName, that.fieldName) + && Objects.equals(pattern, that.pattern) && Objects.equals(matchStrategy, that.matchStrategy); + } + + @Override + public int hashCode() { + return Objects.hash(parseStrategy, fieldName, pattern, matchStrategy); + } + + @Override + public String toString() { + return "GatewayParamFlowItemEntity{" + "parseStrategy=" + parseStrategy + ", fieldName='" + fieldName + '\'' + + ", pattern='" + pattern + '\'' + ", matchStrategy=" + matchStrategy + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java new file mode 100644 index 0000000..43fb72f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java @@ -0,0 +1,115 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; + +import com.alibaba.csp.sentinel.slots.block.AbstractRule; + +import java.util.Date; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +public abstract class AbstractRuleEntity implements RuleEntity { + + protected Long id; + + protected String app; + + protected String ip; + + protected Integer port; + + protected T rule; + + private Date gmtCreate; + + private Date gmtModified; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public AbstractRuleEntity setApp(String app) { + this.app = app; + return this; + } + + @Override + public String getIp() { + return ip; + } + + public AbstractRuleEntity setIp(String ip) { + this.ip = ip; + return this; + } + + @Override + public Integer getPort() { + return port; + } + + public AbstractRuleEntity setPort(Integer port) { + this.port = port; + return this; + } + + public T getRule() { + return rule; + } + + public AbstractRuleEntity setRule(T rule) { + this.rule = rule; + return this; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public AbstractRuleEntity setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + return this; + } + + public Date getGmtModified() { + return gmtModified; + } + + public AbstractRuleEntity setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + return this; + } + + @Override + public T toRule() { + return rule; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java new file mode 100644 index 0000000..a66cb9a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; + +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +public class AuthorityRuleEntity extends AbstractRuleEntity { + + public AuthorityRuleEntity() { + } + + public AuthorityRuleEntity(AuthorityRule authorityRule) { + AssertUtil.notNull(authorityRule, "Authority rule should not be null"); + this.rule = authorityRule; + } + + public static AuthorityRuleEntity fromAuthorityRule(String app, String ip, Integer port, AuthorityRule rule) { + AuthorityRuleEntity entity = new AuthorityRuleEntity(rule); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + return entity; + } + + @JsonIgnore + @JSONField(serialize = false) + public String getLimitApp() { + return rule.getLimitApp(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getResource() { + return rule.getResource(); + } + + @JsonIgnore + @JSONField(serialize = false) + public int getStrategy() { + return rule.getStrategy(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/DegradeRuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/DegradeRuleEntity.java new file mode 100644 index 0000000..4804f0e --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/DegradeRuleEntity.java @@ -0,0 +1,213 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; + +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; + +import java.util.Date; + +/** + * @author leyou + */ +public class DegradeRuleEntity implements RuleEntity { + + private Long id; + + private String app; + + private String ip; + + private Integer port; + + private String resource; + + private String limitApp; + + private Double count; + + private Integer timeWindow; + + private Integer grade; + + private Integer minRequestAmount; + + private Double slowRatioThreshold; + + private Integer statIntervalMs; + + private Date gmtCreate; + + private Date gmtModified; + + public static DegradeRuleEntity fromDegradeRule(String app, String ip, Integer port, DegradeRule rule) { + DegradeRuleEntity entity = new DegradeRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setResource(rule.getResource()); + entity.setLimitApp(rule.getLimitApp()); + entity.setCount(rule.getCount()); + entity.setTimeWindow(rule.getTimeWindow()); + entity.setGrade(rule.getGrade()); + entity.setMinRequestAmount(rule.getMinRequestAmount()); + entity.setSlowRatioThreshold(rule.getSlowRatioThreshold()); + entity.setStatIntervalMs(rule.getStatIntervalMs()); + return entity; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getLimitApp() { + return limitApp; + } + + public void setLimitApp(String limitApp) { + this.limitApp = limitApp; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Integer getTimeWindow() { + return timeWindow; + } + + public void setTimeWindow(Integer timeWindow) { + this.timeWindow = timeWindow; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Integer getMinRequestAmount() { + return minRequestAmount; + } + + public DegradeRuleEntity setMinRequestAmount(Integer minRequestAmount) { + this.minRequestAmount = minRequestAmount; + return this; + } + + public Double getSlowRatioThreshold() { + return slowRatioThreshold; + } + + public DegradeRuleEntity setSlowRatioThreshold(Double slowRatioThreshold) { + this.slowRatioThreshold = slowRatioThreshold; + return this; + } + + public Integer getStatIntervalMs() { + return statIntervalMs; + } + + public DegradeRuleEntity setStatIntervalMs(Integer statIntervalMs) { + this.statIntervalMs = statIntervalMs; + return this; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + @Override + public DegradeRule toRule() { + DegradeRule rule = new DegradeRule(); + rule.setResource(resource); + rule.setLimitApp(limitApp); + rule.setCount(count); + rule.setTimeWindow(timeWindow); + rule.setGrade(grade); + if (minRequestAmount != null) { + rule.setMinRequestAmount(minRequestAmount); + } + if (slowRatioThreshold != null) { + rule.setSlowRatioThreshold(slowRatioThreshold); + } + if (statIntervalMs != null) { + rule.setStatIntervalMs(statIntervalMs); + } + + return rule; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/FlowRuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/FlowRuleEntity.java new file mode 100644 index 0000000..c83cb8c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/FlowRuleEntity.java @@ -0,0 +1,263 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; + +import com.alibaba.csp.sentinel.slots.block.flow.ClusterFlowConfig; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +import java.util.Date; + +/** + * @author leyou + */ +public class FlowRuleEntity implements RuleEntity { + + private Long id; + + private String app; + + private String ip; + + private Integer port; + + private String limitApp; + + private String resource; + + /** + * 0为线程数;1为qps + */ + private Integer grade; + + private Double count; + + /** + * 0为直接限流;1为关联限流;2为链路限流 + ***/ + private Integer strategy; + + private String refResource; + + /** + * 0. default, 1. warm up, 2. rate limiter + */ + private Integer controlBehavior; + + private Integer warmUpPeriodSec; + + /** + * max queueing time in rate limiter behavior + */ + private Integer maxQueueingTimeMs; + + private boolean clusterMode; + + /** + * Flow rule config for cluster mode. + */ + private ClusterFlowConfig clusterConfig; + + private Date gmtCreate; + + private Date gmtModified; + + public static FlowRuleEntity fromFlowRule(String app, String ip, Integer port, FlowRule rule) { + FlowRuleEntity entity = new FlowRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setLimitApp(rule.getLimitApp()); + entity.setResource(rule.getResource()); + entity.setGrade(rule.getGrade()); + entity.setCount(rule.getCount()); + entity.setStrategy(rule.getStrategy()); + entity.setRefResource(rule.getRefResource()); + entity.setControlBehavior(rule.getControlBehavior()); + entity.setWarmUpPeriodSec(rule.getWarmUpPeriodSec()); + entity.setMaxQueueingTimeMs(rule.getMaxQueueingTimeMs()); + entity.setClusterMode(rule.isClusterMode()); + entity.setClusterConfig(rule.getClusterConfig()); + return entity; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public String getLimitApp() { + return limitApp; + } + + public void setLimitApp(String limitApp) { + this.limitApp = limitApp; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Integer getStrategy() { + return strategy; + } + + public void setStrategy(Integer strategy) { + this.strategy = strategy; + } + + public String getRefResource() { + return refResource; + } + + public void setRefResource(String refResource) { + this.refResource = refResource; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getWarmUpPeriodSec() { + return warmUpPeriodSec; + } + + public void setWarmUpPeriodSec(Integer warmUpPeriodSec) { + this.warmUpPeriodSec = warmUpPeriodSec; + } + + public Integer getMaxQueueingTimeMs() { + return maxQueueingTimeMs; + } + + public void setMaxQueueingTimeMs(Integer maxQueueingTimeMs) { + this.maxQueueingTimeMs = maxQueueingTimeMs; + } + + public boolean isClusterMode() { + return clusterMode; + } + + public FlowRuleEntity setClusterMode(boolean clusterMode) { + this.clusterMode = clusterMode; + return this; + } + + public ClusterFlowConfig getClusterConfig() { + return clusterConfig; + } + + public FlowRuleEntity setClusterConfig(ClusterFlowConfig clusterConfig) { + this.clusterConfig = clusterConfig; + return this; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + @Override + public FlowRule toRule() { + FlowRule flowRule = new FlowRule(); + flowRule.setCount(this.count); + flowRule.setGrade(this.grade); + flowRule.setResource(this.resource); + flowRule.setLimitApp(this.limitApp); + flowRule.setRefResource(this.refResource); + flowRule.setStrategy(this.strategy); + if (this.controlBehavior != null) { + flowRule.setControlBehavior(controlBehavior); + } + if (this.warmUpPeriodSec != null) { + flowRule.setWarmUpPeriodSec(warmUpPeriodSec); + } + if (this.maxQueueingTimeMs != null) { + flowRule.setMaxQueueingTimeMs(maxQueueingTimeMs); + } + flowRule.setClusterMode(clusterMode); + flowRule.setClusterConfig(clusterConfig); + return flowRule; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java new file mode 100644 index 0000000..b5a2977 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java @@ -0,0 +1,121 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; + +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowClusterConfig; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowItem; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.util.List; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +public class ParamFlowRuleEntity extends AbstractRuleEntity { + + public ParamFlowRuleEntity() { + } + + public ParamFlowRuleEntity(ParamFlowRule rule) { + AssertUtil.notNull(rule, "Authority rule should not be null"); + this.rule = rule; + } + + public static ParamFlowRuleEntity fromAuthorityRule(String app, String ip, Integer port, ParamFlowRule rule) { + ParamFlowRuleEntity entity = new ParamFlowRuleEntity(rule); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + return entity; + } + + @JsonIgnore + @JSONField(serialize = false) + public String getLimitApp() { + return rule.getLimitApp(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getResource() { + return rule.getResource(); + } + + @JsonIgnore + @JSONField(serialize = false) + public int getGrade() { + return rule.getGrade(); + } + + @JsonIgnore + @JSONField(serialize = false) + public Integer getParamIdx() { + return rule.getParamIdx(); + } + + @JsonIgnore + @JSONField(serialize = false) + public double getCount() { + return rule.getCount(); + } + + @JsonIgnore + @JSONField(serialize = false) + public List getParamFlowItemList() { + return rule.getParamFlowItemList(); + } + + @JsonIgnore + @JSONField(serialize = false) + public int getControlBehavior() { + return rule.getControlBehavior(); + } + + @JsonIgnore + @JSONField(serialize = false) + public int getMaxQueueingTimeMs() { + return rule.getMaxQueueingTimeMs(); + } + + @JsonIgnore + @JSONField(serialize = false) + public int getBurstCount() { + return rule.getBurstCount(); + } + + @JsonIgnore + @JSONField(serialize = false) + public long getDurationInSec() { + return rule.getDurationInSec(); + } + + @JsonIgnore + @JSONField(serialize = false) + public boolean isClusterMode() { + return rule.isClusterMode(); + } + + @JsonIgnore + @JSONField(serialize = false) + public ParamFlowClusterConfig getClusterConfig() { + return rule.getClusterConfig(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/RuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/RuleEntity.java new file mode 100644 index 0000000..52d7e0f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/RuleEntity.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; + +import com.alibaba.csp.sentinel.slots.block.Rule; + +import java.util.Date; + +/** + * @author leyou + */ +public interface RuleEntity { + + Long getId(); + + void setId(Long id); + + String getApp(); + + String getIp(); + + Integer getPort(); + + Date getGmtCreate(); + + Rule toRule(); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java new file mode 100644 index 0000000..a147d3f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java @@ -0,0 +1,167 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; + +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +import java.util.Date; + +/** + * @author leyou + */ +public class SystemRuleEntity implements RuleEntity { + + private Long id; + + private String app; + + private String ip; + + private Integer port; + + private Double highestSystemLoad; + + private Long avgRt; + + private Long maxThread; + + private Double qps; + + private Double highestCpuUsage; + + private Date gmtCreate; + + private Date gmtModified; + + public static SystemRuleEntity fromSystemRule(String app, String ip, Integer port, SystemRule rule) { + SystemRuleEntity entity = new SystemRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setHighestSystemLoad(rule.getHighestSystemLoad()); + entity.setHighestCpuUsage(rule.getHighestCpuUsage()); + entity.setAvgRt(rule.getAvgRt()); + entity.setMaxThread(rule.getMaxThread()); + entity.setQps(rule.getQps()); + return entity; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Double getHighestSystemLoad() { + return highestSystemLoad; + } + + public void setHighestSystemLoad(Double highestSystemLoad) { + this.highestSystemLoad = highestSystemLoad; + } + + public Long getAvgRt() { + return avgRt; + } + + public void setAvgRt(Long avgRt) { + this.avgRt = avgRt; + } + + public Long getMaxThread() { + return maxThread; + } + + public void setMaxThread(Long maxThread) { + this.maxThread = maxThread; + } + + public Double getQps() { + return qps; + } + + public void setQps(Double qps) { + this.qps = qps; + } + + public Double getHighestCpuUsage() { + return highestCpuUsage; + } + + public void setHighestCpuUsage(Double highestCpuUsage) { + this.highestCpuUsage = highestCpuUsage; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + @Override + public SystemRule toRule() { + SystemRule rule = new SystemRule(); + rule.setHighestSystemLoad(highestSystemLoad); + rule.setAvgRt(avgRt); + rule.setMaxThread(maxThread); + rule.setQps(qps); + rule.setHighestCpuUsage(highestCpuUsage); + return rule; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java new file mode 100644 index 0000000..463412c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java @@ -0,0 +1,125 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.discovery; + +import com.alibaba.csp.sentinel.dashboard.config.DashboardConfig; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class AppInfo { + + private String app = ""; + + private Integer appType = 0; + + private Set machines = ConcurrentHashMap.newKeySet(); + + public AppInfo() { + } + + public AppInfo(String app) { + this.app = app; + } + + public AppInfo(String app, Integer appType) { + this.app = app; + this.appType = appType; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + + /** + * Get the current machines. + * @return a new copy of the current machines. + */ + public Set getMachines() { + return new HashSet<>(machines); + } + + @Override + public String toString() { + return "AppInfo{" + "app='" + app + ", machines=" + machines + '}'; + } + + public boolean addMachine(MachineInfo machineInfo) { + machines.remove(machineInfo); + return machines.add(machineInfo); + } + + public synchronized boolean removeMachine(String ip, int port) { + Iterator it = machines.iterator(); + while (it.hasNext()) { + MachineInfo machine = it.next(); + if (machine.getIp().equals(ip) && machine.getPort() == port) { + it.remove(); + return true; + } + } + return false; + } + + public Optional getMachine(String ip, int port) { + return machines.stream().filter(e -> e.getIp().equals(ip) && e.getPort().equals(port)).findFirst(); + } + + private boolean heartbeatJudge(final int threshold) { + if (machines.size() == 0) { + return false; + } + if (threshold > 0) { + long healthyCount = machines.stream().filter(MachineInfo::isHealthy).count(); + if (healthyCount == 0) { + // No healthy machines. + return machines.stream().max(Comparator.comparingLong(MachineInfo::getLastHeartbeat)) + .map(e -> System.currentTimeMillis() - e.getLastHeartbeat() < threshold).orElse(false); + } + } + return true; + } + + /** + * Check whether current application has no healthy machines and should not be + * displayed. + * @return true if the application should be displayed in the sidebar, otherwise false + */ + public boolean isShown() { + return heartbeatJudge(DashboardConfig.getHideAppNoMachineMillis()); + } + + /** + * Check whether current application has no healthy machines and should be removed. + * @return true if the application is dead and should be removed, otherwise false + */ + public boolean isDead() { + return !heartbeatJudge(DashboardConfig.getRemoveAppNoMachineMillis()); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppManagement.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppManagement.java new file mode 100644 index 0000000..7733a51 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppManagement.java @@ -0,0 +1,69 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.discovery; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.Set; + +@Component +public class AppManagement implements MachineDiscovery { + + @Autowired + private ApplicationContext context; + + private MachineDiscovery machineDiscovery; + + @PostConstruct + public void init() { + machineDiscovery = context.getBean(SimpleMachineDiscovery.class); + } + + @Override + public Set getBriefApps() { + return machineDiscovery.getBriefApps(); + } + + @Override + public long addMachine(MachineInfo machineInfo) { + return machineDiscovery.addMachine(machineInfo); + } + + @Override + public boolean removeMachine(String app, String ip, int port) { + return machineDiscovery.removeMachine(app, ip, port); + } + + @Override + public List getAppNames() { + return machineDiscovery.getAppNames(); + } + + @Override + public AppInfo getDetailApp(String app) { + return machineDiscovery.getDetailApp(app); + } + + @Override + public void removeApp(String app) { + machineDiscovery.removeApp(app); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineDiscovery.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineDiscovery.java new file mode 100644 index 0000000..454e55a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineDiscovery.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.discovery; + +import java.util.List; +import java.util.Set; + +public interface MachineDiscovery { + + String UNKNOWN_APP_NAME = "CLUSTER_NOT_STARTED"; + + List getAppNames(); + + Set getBriefApps(); + + AppInfo getDetailApp(String app); + + /** + * Remove the given app from the application registry. + * @param app application name + * @since 1.5.0 + */ + void removeApp(String app); + + long addMachine(MachineInfo machineInfo); + + /** + * Remove the given machine instance from the application registry. + * @param app the application name of the machine + * @param ip machine IP + * @param port machine port + * @return true if removed, otherwise false + * @since 1.5.0 + */ + boolean removeMachine(String app, String ip, int port); + +} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java new file mode 100644 index 0000000..2b4a080 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java @@ -0,0 +1,186 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.discovery; + +import com.alibaba.csp.sentinel.dashboard.config.DashboardConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.Objects; + +public class MachineInfo implements Comparable { + + private String app = ""; + + private Integer appType = 0; + + private String hostname = ""; + + private String ip = ""; + + private Integer port = -1; + + private long lastHeartbeat; + + private long heartbeatVersion; + + /** + * Indicates the version of Sentinel client (since 0.2.0). + */ + private String version; + + public static MachineInfo of(String app, String ip, Integer port) { + MachineInfo machineInfo = new MachineInfo(); + machineInfo.setApp(app); + machineInfo.setIp(ip); + machineInfo.setPort(port); + return machineInfo; + } + + public String toHostPort() { + return ip + ":" + port; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public long getHeartbeatVersion() { + return heartbeatVersion; + } + + public void setHeartbeatVersion(long heartbeatVersion) { + this.heartbeatVersion = heartbeatVersion; + } + + public String getVersion() { + return version; + } + + public MachineInfo setVersion(String version) { + this.version = version; + return this; + } + + public boolean isHealthy() { + long delta = System.currentTimeMillis() - lastHeartbeat; + return delta < DashboardConfig.getUnhealthyMachineMillis(); + } + + /** + * whether dead should be removed + * @return + */ + public boolean isDead() { + if (DashboardConfig.getAutoRemoveMachineMillis() > 0) { + long delta = System.currentTimeMillis() - lastHeartbeat; + return delta > DashboardConfig.getAutoRemoveMachineMillis(); + } + return false; + } + + public long getLastHeartbeat() { + return lastHeartbeat; + } + + public void setLastHeartbeat(long lastHeartbeat) { + this.lastHeartbeat = lastHeartbeat; + } + + @Override + public int compareTo(MachineInfo o) { + if (this == o) { + return 0; + } + if (!port.equals(o.getPort())) { + return port.compareTo(o.getPort()); + } + if (!StringUtil.equals(app, o.getApp())) { + return app.compareToIgnoreCase(o.getApp()); + } + return ip.compareToIgnoreCase(o.getIp()); + } + + @Override + public String toString() { + return new StringBuilder("MachineInfo {").append("app='").append(app).append('\'').append(",appType='") + .append(appType).append('\'').append(", hostname='").append(hostname).append('\'').append(", ip='") + .append(ip).append('\'').append(", port=").append(port).append(", heartbeatVersion=") + .append(heartbeatVersion).append(", lastHeartbeat=").append(lastHeartbeat).append(", version='") + .append(version).append('\'').append(", healthy=").append(isHealthy()).append('}').toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MachineInfo)) { + return false; + } + MachineInfo that = (MachineInfo) o; + return Objects.equals(app, that.app) && Objects.equals(ip, that.ip) && Objects.equals(port, that.port); + } + + @Override + public int hashCode() { + return Objects.hash(app, ip, port); + } + + /** + * Information for log + * @return + */ + public String toLogString() { + return app + "|" + ip + "|" + port + "|" + version; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java new file mode 100644 index 0000000..5d06c5f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java @@ -0,0 +1,77 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.discovery; + +import com.alibaba.csp.sentinel.util.AssertUtil; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author leyou + */ +@Component +public class SimpleMachineDiscovery implements MachineDiscovery { + + private final ConcurrentMap apps = new ConcurrentHashMap<>(); + + @Override + public long addMachine(MachineInfo machineInfo) { + AssertUtil.notNull(machineInfo, "machineInfo cannot be null"); + AppInfo appInfo = apps.computeIfAbsent(machineInfo.getApp(), + o -> new AppInfo(machineInfo.getApp(), machineInfo.getAppType())); + appInfo.addMachine(machineInfo); + return 1; + } + + @Override + public boolean removeMachine(String app, String ip, int port) { + AssertUtil.assertNotBlank(app, "app name cannot be blank"); + AppInfo appInfo = apps.get(app); + if (appInfo != null) { + return appInfo.removeMachine(ip, port); + } + return false; + } + + @Override + public List getAppNames() { + return new ArrayList<>(apps.keySet()); + } + + @Override + public AppInfo getDetailApp(String app) { + AssertUtil.assertNotBlank(app, "app name cannot be blank"); + return apps.get(app); + } + + @Override + public Set getBriefApps() { + return new HashSet<>(apps.values()); + } + + @Override + public void removeApp(String app) { + AssertUtil.assertNotBlank(app, "app name cannot be blank"); + apps.remove(app); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java new file mode 100644 index 0000000..bb6dead --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java @@ -0,0 +1,257 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain; + +import com.alibaba.csp.sentinel.command.vo.NodeVo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author leyou + */ +public class ResourceTreeNode { + + private String id; + + private String parentId; + + private String resource; + + private Integer threadNum; + + private Long passQps; + + private Long blockQps; + + private Long totalQps; + + private Long averageRt; + + private Long successQps; + + private Long exceptionQps; + + private Long oneMinutePass; + + private Long oneMinuteBlock; + + private Long oneMinuteException; + + private Long oneMinuteTotal; + + private boolean visible = true; + + private List children = new ArrayList<>(); + + public static ResourceTreeNode fromNodeVoList(List nodeVos) { + if (nodeVos == null || nodeVos.isEmpty()) { + return null; + } + ResourceTreeNode root = null; + Map map = new HashMap<>(); + for (NodeVo vo : nodeVos) { + ResourceTreeNode node = fromNodeVo(vo); + map.put(node.id, node); + // real root + if (node.parentId == null || node.parentId.isEmpty()) { + root = node; + } + else if (map.containsKey(node.parentId)) { + map.get(node.parentId).children.add(node); + } + else { + // impossible + } + } + return root; + } + + public static ResourceTreeNode fromNodeVo(NodeVo vo) { + ResourceTreeNode node = new ResourceTreeNode(); + node.id = vo.getId(); + node.parentId = vo.getParentId(); + node.resource = vo.getResource(); + node.threadNum = vo.getThreadNum(); + node.passQps = vo.getPassQps(); + node.blockQps = vo.getBlockQps(); + node.totalQps = vo.getTotalQps(); + node.averageRt = vo.getAverageRt(); + node.successQps = vo.getSuccessQps(); + node.exceptionQps = vo.getExceptionQps(); + node.oneMinutePass = vo.getOneMinutePass(); + node.oneMinuteBlock = vo.getOneMinuteBlock(); + node.oneMinuteException = vo.getOneMinuteException(); + node.oneMinuteTotal = vo.getOneMinuteTotal(); + return node; + } + + public void searchIgnoreCase(String searchKey) { + search(this, searchKey); + } + + /** + * This node is visible only when searchKey matches this.resource or at least one of + * this's children is visible + */ + private boolean search(ResourceTreeNode node, String searchKey) { + // empty matches all + if (searchKey == null || searchKey.isEmpty() || node.resource.toLowerCase().contains(searchKey.toLowerCase())) { + node.visible = true; + } + else { + node.visible = false; + } + + boolean found = false; + for (ResourceTreeNode c : node.children) { + found |= search(c, searchKey); + } + node.visible |= found; + return node.visible; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getThreadNum() { + return threadNum; + } + + public void setThreadNum(Integer threadNum) { + this.threadNum = threadNum; + } + + public Long getPassQps() { + return passQps; + } + + public void setPassQps(Long passQps) { + this.passQps = passQps; + } + + public Long getBlockQps() { + return blockQps; + } + + public void setBlockQps(Long blockQps) { + this.blockQps = blockQps; + } + + public Long getTotalQps() { + return totalQps; + } + + public void setTotalQps(Long totalQps) { + this.totalQps = totalQps; + } + + public Long getAverageRt() { + return averageRt; + } + + public void setAverageRt(Long averageRt) { + this.averageRt = averageRt; + } + + public Long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(Long successQps) { + this.successQps = successQps; + } + + public Long getExceptionQps() { + return exceptionQps; + } + + public void setExceptionQps(Long exceptionQps) { + this.exceptionQps = exceptionQps; + } + + public Long getOneMinutePass() { + return oneMinutePass; + } + + public void setOneMinutePass(Long oneMinutePass) { + this.oneMinutePass = oneMinutePass; + } + + public Long getOneMinuteBlock() { + return oneMinuteBlock; + } + + public void setOneMinuteBlock(Long oneMinuteBlock) { + this.oneMinuteBlock = oneMinuteBlock; + } + + public Long getOneMinuteException() { + return oneMinuteException; + } + + public void setOneMinuteException(Long oneMinuteException) { + this.oneMinuteException = oneMinuteException; + } + + public Long getOneMinuteTotal() { + return oneMinuteTotal; + } + + public void setOneMinuteTotal(Long oneMinuteTotal) { + this.oneMinuteTotal = oneMinuteTotal; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/Result.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/Result.java new file mode 100644 index 0000000..ca8246d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/Result.java @@ -0,0 +1,97 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain; + +/** + * @author leyou + * @author Eric Zhao + */ +public class Result { + + private boolean success; + + private int code; + + private String msg; + + private R data; + + public static Result ofSuccess(R data) { + return new Result().setSuccess(true).setMsg("success").setData(data); + } + + public static Result ofSuccessMsg(String msg) { + return new Result().setSuccess(true).setMsg(msg); + } + + public static Result ofFail(int code, String msg) { + Result result = new Result<>(); + result.setSuccess(false); + result.setCode(code); + result.setMsg(msg); + return result; + } + + public static Result ofThrowable(int code, Throwable throwable) { + Result result = new Result<>(); + result.setSuccess(false); + result.setCode(code); + result.setMsg(throwable.getClass().getName() + ", " + throwable.getMessage()); + return result; + } + + public boolean isSuccess() { + return success; + } + + public Result setSuccess(boolean success) { + this.success = success; + return this; + } + + public int getCode() { + return code; + } + + public Result setCode(int code) { + this.code = code; + return this; + } + + public String getMsg() { + return msg; + } + + public Result setMsg(String msg) { + this.msg = msg; + return this; + } + + public R getData() { + return data; + } + + public Result setData(R data) { + this.data = data; + return this; + } + + @Override + public String toString() { + return "Result{" + "success=" + success + ", code=" + code + ", msg='" + msg + '\'' + ", data=" + data + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppAssignResultVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppAssignResultVO.java new file mode 100644 index 0000000..74467f7 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppAssignResultVO.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterAppAssignResultVO { + + private Set failedServerSet; + + private Set failedClientSet; + + private Integer totalCount; + + public Set getFailedServerSet() { + return failedServerSet; + } + + public ClusterAppAssignResultVO setFailedServerSet(Set failedServerSet) { + this.failedServerSet = failedServerSet; + return this; + } + + public Set getFailedClientSet() { + return failedClientSet; + } + + public ClusterAppAssignResultVO setFailedClientSet(Set failedClientSet) { + this.failedClientSet = failedClientSet; + return this; + } + + public Integer getTotalCount() { + return totalCount; + } + + public ClusterAppAssignResultVO setTotalCount(Integer totalCount) { + this.totalCount = totalCount; + return this; + } + + @Override + public String toString() { + return "ClusterAppAssignResultVO{" + "failedServerSet=" + failedServerSet + ", failedClientSet=" + + failedClientSet + ", totalCount=" + totalCount + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppFullAssignRequest.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppFullAssignRequest.java new file mode 100644 index 0000000..2e09608 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppFullAssignRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterAppAssignMap; + +import java.util.List; +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterAppFullAssignRequest { + + private List clusterMap; + + private Set remainingList; + + public List getClusterMap() { + return clusterMap; + } + + public ClusterAppFullAssignRequest setClusterMap(List clusterMap) { + this.clusterMap = clusterMap; + return this; + } + + public Set getRemainingList() { + return remainingList; + } + + public ClusterAppFullAssignRequest setRemainingList(Set remainingList) { + this.remainingList = remainingList; + return this; + } + + @Override + public String toString() { + return "ClusterAppFullAssignRequest{" + "clusterMap=" + clusterMap + ", remainingList=" + remainingList + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppSingleServerAssignRequest.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppSingleServerAssignRequest.java new file mode 100644 index 0000000..3b5b2ac --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterAppSingleServerAssignRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterAppAssignMap; + +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterAppSingleServerAssignRequest { + + private ClusterAppAssignMap clusterMap; + + private Set remainingList; + + public ClusterAppAssignMap getClusterMap() { + return clusterMap; + } + + public ClusterAppSingleServerAssignRequest setClusterMap(ClusterAppAssignMap clusterMap) { + this.clusterMap = clusterMap; + return this; + } + + public Set getRemainingList() { + return remainingList; + } + + public ClusterAppSingleServerAssignRequest setRemainingList(Set remainingList) { + this.remainingList = remainingList; + return this; + } + + @Override + public String toString() { + return "ClusterAppSingleServerAssignRequest{" + "clusterMap=" + clusterMap + ", remainingList=" + remainingList + + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterClientInfoVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterClientInfoVO.java new file mode 100644 index 0000000..b2c6299 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterClientInfoVO.java @@ -0,0 +1,74 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterClientInfoVO { + + private String serverHost; + + private Integer serverPort; + + private Integer clientState; + + private Integer requestTimeout; + + public String getServerHost() { + return serverHost; + } + + public ClusterClientInfoVO setServerHost(String serverHost) { + this.serverHost = serverHost; + return this; + } + + public Integer getServerPort() { + return serverPort; + } + + public ClusterClientInfoVO setServerPort(Integer serverPort) { + this.serverPort = serverPort; + return this; + } + + public Integer getClientState() { + return clientState; + } + + public ClusterClientInfoVO setClientState(Integer clientState) { + this.clientState = clientState; + return this; + } + + public Integer getRequestTimeout() { + return requestTimeout; + } + + public ClusterClientInfoVO setRequestTimeout(Integer requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + @Override + public String toString() { + return "ClusterClientInfoVO{" + "serverHost='" + serverHost + '\'' + ", serverPort=" + serverPort + + ", clientState=" + clientState + ", requestTimeout=" + requestTimeout + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterGroupEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterGroupEntity.java new file mode 100644 index 0000000..99150f5 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterGroupEntity.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterGroupEntity { + + private String machineId; + + private String ip; + + private Integer port; + + private Set clientSet = new HashSet<>(); + + private Boolean belongToApp; + + public String getMachineId() { + return machineId; + } + + public ClusterGroupEntity setMachineId(String machineId) { + this.machineId = machineId; + return this; + } + + public String getIp() { + return ip; + } + + public ClusterGroupEntity setIp(String ip) { + this.ip = ip; + return this; + } + + public Integer getPort() { + return port; + } + + public ClusterGroupEntity setPort(Integer port) { + this.port = port; + return this; + } + + public Set getClientSet() { + return clientSet; + } + + public ClusterGroupEntity setClientSet(Set clientSet) { + this.clientSet = clientSet; + return this; + } + + public Boolean getBelongToApp() { + return belongToApp; + } + + public ClusterGroupEntity setBelongToApp(Boolean belongToApp) { + this.belongToApp = belongToApp; + return this; + } + + @Override + public String toString() { + return "ClusterGroupEntity{" + "machineId='" + machineId + '\'' + ", ip='" + ip + '\'' + ", port=" + port + + ", clientSet=" + clientSet + ", belongToApp=" + belongToApp + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterStateSingleVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterStateSingleVO.java new file mode 100644 index 0000000..032efa0 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ClusterStateSingleVO.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterStateSingleVO { + + private String address; + + private Integer mode; + + private String target; + + public String getAddress() { + return address; + } + + public ClusterStateSingleVO setAddress(String address) { + this.address = address; + return this; + } + + public Integer getMode() { + return mode; + } + + public ClusterStateSingleVO setMode(Integer mode) { + this.mode = mode; + return this; + } + + public String getTarget() { + return target; + } + + public ClusterStateSingleVO setTarget(String target) { + this.target = target; + return this; + } + + @Override + public String toString() { + return "ClusterStateSingleVO{" + "address='" + address + '\'' + ", mode=" + mode + ", target='" + target + '\'' + + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionDescriptorVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionDescriptorVO.java new file mode 100644 index 0000000..3ac35a7 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionDescriptorVO.java @@ -0,0 +1,51 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ConnectionDescriptorVO { + + private String address; + + private String host; + + public String getAddress() { + return address; + } + + public ConnectionDescriptorVO setAddress(String address) { + this.address = address; + return this; + } + + public String getHost() { + return host; + } + + public ConnectionDescriptorVO setHost(String host) { + this.host = host; + return this; + } + + @Override + public String toString() { + return "ConnectionDescriptorVO{" + "address='" + address + '\'' + ", host='" + host + '\'' + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionGroupVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionGroupVO.java new file mode 100644 index 0000000..d75a07c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/ConnectionGroupVO.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster; + +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ConnectionGroupVO { + + private String namespace; + + private List connectionSet; + + private Integer connectedCount; + + public String getNamespace() { + return namespace; + } + + public ConnectionGroupVO setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + public List getConnectionSet() { + return connectionSet; + } + + public ConnectionGroupVO setConnectionSet(List connectionSet) { + this.connectionSet = connectionSet; + return this; + } + + public Integer getConnectedCount() { + return connectedCount; + } + + public ConnectionGroupVO setConnectedCount(Integer connectedCount) { + this.connectedCount = connectedCount; + return this; + } + + @Override + public String toString() { + return "ConnectionGroupVO{" + "namespace='" + namespace + '\'' + ", connectionSet=" + connectionSet + + ", connectedCount=" + connectedCount + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ClusterClientConfig.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ClusterClientConfig.java new file mode 100644 index 0000000..e626bac --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ClusterClientConfig.java @@ -0,0 +1,74 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.config; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ClusterClientConfig { + + private String serverHost; + + private Integer serverPort; + + private Integer requestTimeout; + + private Integer connectTimeout; + + public String getServerHost() { + return serverHost; + } + + public ClusterClientConfig setServerHost(String serverHost) { + this.serverHost = serverHost; + return this; + } + + public Integer getServerPort() { + return serverPort; + } + + public ClusterClientConfig setServerPort(Integer serverPort) { + this.serverPort = serverPort; + return this; + } + + public Integer getRequestTimeout() { + return requestTimeout; + } + + public ClusterClientConfig setRequestTimeout(Integer requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + public Integer getConnectTimeout() { + return connectTimeout; + } + + public ClusterClientConfig setConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + @Override + public String toString() { + return "ClusterClientConfig{" + "serverHost='" + serverHost + '\'' + ", serverPort=" + serverPort + + ", requestTimeout=" + requestTimeout + ", connectTimeout=" + connectTimeout + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerFlowConfig.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerFlowConfig.java new file mode 100644 index 0000000..578266d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerFlowConfig.java @@ -0,0 +1,110 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.config; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ServerFlowConfig { + + public static final double DEFAULT_EXCEED_COUNT = 1.0d; + + public static final double DEFAULT_MAX_OCCUPY_RATIO = 1.0d; + + public static final int DEFAULT_INTERVAL_MS = 1000; + + public static final int DEFAULT_SAMPLE_COUNT = 10; + + public static final double DEFAULT_MAX_ALLOWED_QPS = 30000; + + private final String namespace; + + private Double exceedCount = DEFAULT_EXCEED_COUNT; + + private Double maxOccupyRatio = DEFAULT_MAX_OCCUPY_RATIO; + + private Integer intervalMs = DEFAULT_INTERVAL_MS; + + private Integer sampleCount = DEFAULT_SAMPLE_COUNT; + + private Double maxAllowedQps = DEFAULT_MAX_ALLOWED_QPS; + + public ServerFlowConfig() { + this("default"); + } + + public ServerFlowConfig(String namespace) { + this.namespace = namespace; + } + + public String getNamespace() { + return namespace; + } + + public Double getExceedCount() { + return exceedCount; + } + + public ServerFlowConfig setExceedCount(Double exceedCount) { + this.exceedCount = exceedCount; + return this; + } + + public Double getMaxOccupyRatio() { + return maxOccupyRatio; + } + + public ServerFlowConfig setMaxOccupyRatio(Double maxOccupyRatio) { + this.maxOccupyRatio = maxOccupyRatio; + return this; + } + + public Integer getIntervalMs() { + return intervalMs; + } + + public ServerFlowConfig setIntervalMs(Integer intervalMs) { + this.intervalMs = intervalMs; + return this; + } + + public Integer getSampleCount() { + return sampleCount; + } + + public ServerFlowConfig setSampleCount(Integer sampleCount) { + this.sampleCount = sampleCount; + return this; + } + + public Double getMaxAllowedQps() { + return maxAllowedQps; + } + + public ServerFlowConfig setMaxAllowedQps(Double maxAllowedQps) { + this.maxAllowedQps = maxAllowedQps; + return this; + } + + @Override + public String toString() { + return "ServerFlowConfig{" + "namespace='" + namespace + '\'' + ", exceedCount=" + exceedCount + + ", maxOccupyRatio=" + maxOccupyRatio + ", intervalMs=" + intervalMs + ", sampleCount=" + sampleCount + + ", maxAllowedQps=" + maxAllowedQps + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerTransportConfig.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerTransportConfig.java new file mode 100644 index 0000000..76891ab --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/config/ServerTransportConfig.java @@ -0,0 +1,64 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.config; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ServerTransportConfig { + + public static final int DEFAULT_PORT = 18730; + + public static final int DEFAULT_IDLE_SECONDS = 600; + + private Integer port; + + private Integer idleSeconds; + + public ServerTransportConfig() { + this(DEFAULT_PORT, DEFAULT_IDLE_SECONDS); + } + + public ServerTransportConfig(Integer port, Integer idleSeconds) { + this.port = port; + this.idleSeconds = idleSeconds; + } + + public Integer getPort() { + return port; + } + + public ServerTransportConfig setPort(Integer port) { + this.port = port; + return this; + } + + public Integer getIdleSeconds() { + return idleSeconds; + } + + public ServerTransportConfig setIdleSeconds(Integer idleSeconds) { + this.idleSeconds = idleSeconds; + return this; + } + + @Override + public String toString() { + return "ServerTransportConfig{" + "port=" + port + ", idleSeconds=" + idleSeconds + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterAppAssignMap.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterAppAssignMap.java new file mode 100644 index 0000000..23c78c9 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterAppAssignMap.java @@ -0,0 +1,110 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.request; + +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterAppAssignMap { + + private String machineId; + + private String ip; + + private Integer port; + + private Boolean belongToApp; + + private Set clientSet; + + private Set namespaceSet; + + private Double maxAllowedQps; + + public String getMachineId() { + return machineId; + } + + public ClusterAppAssignMap setMachineId(String machineId) { + this.machineId = machineId; + return this; + } + + public String getIp() { + return ip; + } + + public ClusterAppAssignMap setIp(String ip) { + this.ip = ip; + return this; + } + + public Integer getPort() { + return port; + } + + public ClusterAppAssignMap setPort(Integer port) { + this.port = port; + return this; + } + + public Set getClientSet() { + return clientSet; + } + + public ClusterAppAssignMap setClientSet(Set clientSet) { + this.clientSet = clientSet; + return this; + } + + public Set getNamespaceSet() { + return namespaceSet; + } + + public ClusterAppAssignMap setNamespaceSet(Set namespaceSet) { + this.namespaceSet = namespaceSet; + return this; + } + + public Boolean getBelongToApp() { + return belongToApp; + } + + public ClusterAppAssignMap setBelongToApp(Boolean belongToApp) { + this.belongToApp = belongToApp; + return this; + } + + public Double getMaxAllowedQps() { + return maxAllowedQps; + } + + public ClusterAppAssignMap setMaxAllowedQps(Double maxAllowedQps) { + this.maxAllowedQps = maxAllowedQps; + return this; + } + + @Override + public String toString() { + return "ClusterAppAssignMap{" + "machineId='" + machineId + '\'' + ", ip='" + ip + '\'' + ", port=" + port + + ", belongToApp=" + belongToApp + ", clientSet=" + clientSet + ", namespaceSet=" + namespaceSet + + ", maxAllowedQps=" + maxAllowedQps + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterClientModifyRequest.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterClientModifyRequest.java new file mode 100644 index 0000000..9a7608a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterClientModifyRequest.java @@ -0,0 +1,85 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.request; + +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ClusterClientConfig; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ClusterClientModifyRequest implements ClusterModifyRequest { + + private String app; + + private String ip; + + private Integer port; + + private Integer mode; + + private ClusterClientConfig clientConfig; + + @Override + public String getApp() { + return app; + } + + public ClusterClientModifyRequest setApp(String app) { + this.app = app; + return this; + } + + @Override + public String getIp() { + return ip; + } + + public ClusterClientModifyRequest setIp(String ip) { + this.ip = ip; + return this; + } + + @Override + public Integer getPort() { + return port; + } + + public ClusterClientModifyRequest setPort(Integer port) { + this.port = port; + return this; + } + + @Override + public Integer getMode() { + return mode; + } + + public ClusterClientModifyRequest setMode(Integer mode) { + this.mode = mode; + return this; + } + + public ClusterClientConfig getClientConfig() { + return clientConfig; + } + + public ClusterClientModifyRequest setClientConfig(ClusterClientConfig clientConfig) { + this.clientConfig = clientConfig; + return this; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterModifyRequest.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterModifyRequest.java new file mode 100644 index 0000000..e0d48e6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterModifyRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.request; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public interface ClusterModifyRequest { + + String getApp(); + + String getIp(); + + Integer getPort(); + + Integer getMode(); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterServerModifyRequest.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterServerModifyRequest.java new file mode 100644 index 0000000..02c96c2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/request/ClusterServerModifyRequest.java @@ -0,0 +1,117 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.request; + +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerFlowConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerTransportConfig; + +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ClusterServerModifyRequest implements ClusterModifyRequest { + + private String app; + + private String ip; + + private Integer port; + + private Integer mode; + + private ServerFlowConfig flowConfig; + + private ServerTransportConfig transportConfig; + + private Set namespaceSet; + + @Override + public String getApp() { + return app; + } + + public ClusterServerModifyRequest setApp(String app) { + this.app = app; + return this; + } + + @Override + public String getIp() { + return ip; + } + + public ClusterServerModifyRequest setIp(String ip) { + this.ip = ip; + return this; + } + + @Override + public Integer getPort() { + return port; + } + + public ClusterServerModifyRequest setPort(Integer port) { + this.port = port; + return this; + } + + @Override + public Integer getMode() { + return mode; + } + + public ClusterServerModifyRequest setMode(Integer mode) { + this.mode = mode; + return this; + } + + public ServerFlowConfig getFlowConfig() { + return flowConfig; + } + + public ClusterServerModifyRequest setFlowConfig(ServerFlowConfig flowConfig) { + this.flowConfig = flowConfig; + return this; + } + + public ServerTransportConfig getTransportConfig() { + return transportConfig; + } + + public ClusterServerModifyRequest setTransportConfig(ServerTransportConfig transportConfig) { + this.transportConfig = transportConfig; + return this; + } + + public Set getNamespaceSet() { + return namespaceSet; + } + + public ClusterServerModifyRequest setNamespaceSet(Set namespaceSet) { + this.namespaceSet = namespaceSet; + return this; + } + + @Override + public String toString() { + return "ClusterServerModifyRequest{" + "app='" + app + '\'' + ", ip='" + ip + '\'' + ", port=" + port + + ", mode=" + mode + ", flowConfig=" + flowConfig + ", transportConfig=" + transportConfig + + ", namespaceSet=" + namespaceSet + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterClientStateWrapVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterClientStateWrapVO.java new file mode 100644 index 0000000..19daa98 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterClientStateWrapVO.java @@ -0,0 +1,77 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class AppClusterClientStateWrapVO { + + /** + * {ip}@{transport_command_port}. + */ + private String id; + + private Integer commandPort; + + private String ip; + + private ClusterClientStateVO state; + + public String getId() { + return id; + } + + public AppClusterClientStateWrapVO setId(String id) { + this.id = id; + return this; + } + + public String getIp() { + return ip; + } + + public AppClusterClientStateWrapVO setIp(String ip) { + this.ip = ip; + return this; + } + + public ClusterClientStateVO getState() { + return state; + } + + public AppClusterClientStateWrapVO setState(ClusterClientStateVO state) { + this.state = state; + return this; + } + + public Integer getCommandPort() { + return commandPort; + } + + public AppClusterClientStateWrapVO setCommandPort(Integer commandPort) { + this.commandPort = commandPort; + return this; + } + + @Override + public String toString() { + return "AppClusterClientStateWrapVO{" + "id='" + id + '\'' + ", commandPort=" + commandPort + ", ip='" + ip + + '\'' + ", state=" + state + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterServerStateWrapVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterServerStateWrapVO.java new file mode 100644 index 0000000..4f3bbb4 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/AppClusterServerStateWrapVO.java @@ -0,0 +1,99 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class AppClusterServerStateWrapVO { + + /** + * {ip}@{transport_command_port}. + */ + private String id; + + private String ip; + + private Integer port; + + private Integer connectedCount; + + private Boolean belongToApp; + + private ClusterServerStateVO state; + + public String getId() { + return id; + } + + public AppClusterServerStateWrapVO setId(String id) { + this.id = id; + return this; + } + + public String getIp() { + return ip; + } + + public AppClusterServerStateWrapVO setIp(String ip) { + this.ip = ip; + return this; + } + + public Integer getPort() { + return port; + } + + public AppClusterServerStateWrapVO setPort(Integer port) { + this.port = port; + return this; + } + + public Boolean getBelongToApp() { + return belongToApp; + } + + public AppClusterServerStateWrapVO setBelongToApp(Boolean belongToApp) { + this.belongToApp = belongToApp; + return this; + } + + public Integer getConnectedCount() { + return connectedCount; + } + + public AppClusterServerStateWrapVO setConnectedCount(Integer connectedCount) { + this.connectedCount = connectedCount; + return this; + } + + public ClusterServerStateVO getState() { + return state; + } + + public AppClusterServerStateWrapVO setState(ClusterServerStateVO state) { + this.state = state; + return this; + } + + @Override + public String toString() { + return "AppClusterServerStateWrapVO{" + "id='" + id + '\'' + ", ip='" + ip + '\'' + ", port='" + port + '\'' + + ", belongToApp=" + belongToApp + ", state=" + state + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterClientStateVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterClientStateVO.java new file mode 100644 index 0000000..fd2df5f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterClientStateVO.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterClientInfoVO; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ClusterClientStateVO { + + /** + * Cluster token client state. + */ + private ClusterClientInfoVO clientConfig; + + public ClusterClientInfoVO getClientConfig() { + return clientConfig; + } + + public ClusterClientStateVO setClientConfig(ClusterClientInfoVO clientConfig) { + this.clientConfig = clientConfig; + return this; + } + + @Override + public String toString() { + return "ClusterClientStateVO{" + "clientConfig=" + clientConfig + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterRequestLimitVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterRequestLimitVO.java new file mode 100644 index 0000000..aaf10dc --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterRequestLimitVO.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterRequestLimitVO { + + private String namespace; + + private Double currentQps; + + private Double maxAllowedQps; + + public String getNamespace() { + return namespace; + } + + public ClusterRequestLimitVO setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + public Double getCurrentQps() { + return currentQps; + } + + public ClusterRequestLimitVO setCurrentQps(Double currentQps) { + this.currentQps = currentQps; + return this; + } + + public Double getMaxAllowedQps() { + return maxAllowedQps; + } + + public ClusterRequestLimitVO setMaxAllowedQps(Double maxAllowedQps) { + this.maxAllowedQps = maxAllowedQps; + return this; + } + + @Override + public String toString() { + return "ClusterRequestLimitVO{" + "namespace='" + namespace + '\'' + ", currentQps=" + currentQps + + ", maxAllowedQps=" + maxAllowedQps + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterServerStateVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterServerStateVO.java new file mode 100644 index 0000000..dbd6160 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterServerStateVO.java @@ -0,0 +1,126 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ConnectionGroupVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerFlowConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerTransportConfig; + +import java.util.List; +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ClusterServerStateVO { + + private String appName; + + private ServerTransportConfig transport; + + private ServerFlowConfig flow; + + private Set namespaceSet; + + private Integer port; + + private List connection; + + private List requestLimitData; + + private Boolean embedded; + + public String getAppName() { + return appName; + } + + public ClusterServerStateVO setAppName(String appName) { + this.appName = appName; + return this; + } + + public ServerTransportConfig getTransport() { + return transport; + } + + public ClusterServerStateVO setTransport(ServerTransportConfig transport) { + this.transport = transport; + return this; + } + + public ServerFlowConfig getFlow() { + return flow; + } + + public ClusterServerStateVO setFlow(ServerFlowConfig flow) { + this.flow = flow; + return this; + } + + public Set getNamespaceSet() { + return namespaceSet; + } + + public ClusterServerStateVO setNamespaceSet(Set namespaceSet) { + this.namespaceSet = namespaceSet; + return this; + } + + public Integer getPort() { + return port; + } + + public ClusterServerStateVO setPort(Integer port) { + this.port = port; + return this; + } + + public List getConnection() { + return connection; + } + + public ClusterServerStateVO setConnection(List connection) { + this.connection = connection; + return this; + } + + public List getRequestLimitData() { + return requestLimitData; + } + + public ClusterServerStateVO setRequestLimitData(List requestLimitData) { + this.requestLimitData = requestLimitData; + return this; + } + + public Boolean getEmbedded() { + return embedded; + } + + public ClusterServerStateVO setEmbedded(Boolean embedded) { + this.embedded = embedded; + return this; + } + + @Override + public String toString() { + return "ClusterServerStateVO{" + "appName='" + appName + '\'' + ", transport=" + transport + ", flow=" + flow + + ", namespaceSet=" + namespaceSet + ", port=" + port + ", connection=" + connection + + ", requestLimitData=" + requestLimitData + ", embedded=" + embedded + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterStateSimpleEntity.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterStateSimpleEntity.java new file mode 100644 index 0000000..d0b2852 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterStateSimpleEntity.java @@ -0,0 +1,74 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ClusterStateSimpleEntity { + + private Integer mode; + + private Long lastModified; + + private Boolean clientAvailable; + + private Boolean serverAvailable; + + public Integer getMode() { + return mode; + } + + public ClusterStateSimpleEntity setMode(Integer mode) { + this.mode = mode; + return this; + } + + public Long getLastModified() { + return lastModified; + } + + public ClusterStateSimpleEntity setLastModified(Long lastModified) { + this.lastModified = lastModified; + return this; + } + + public Boolean getClientAvailable() { + return clientAvailable; + } + + public ClusterStateSimpleEntity setClientAvailable(Boolean clientAvailable) { + this.clientAvailable = clientAvailable; + return this; + } + + public Boolean getServerAvailable() { + return serverAvailable; + } + + public ClusterStateSimpleEntity setServerAvailable(Boolean serverAvailable) { + this.serverAvailable = serverAvailable; + return this; + } + + @Override + public String toString() { + return "ClusterStateSimpleEntity{" + "mode=" + mode + ", lastModified=" + lastModified + ", clientAvailable=" + + clientAvailable + ", serverAvailable=" + serverAvailable + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStatePairVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStatePairVO.java new file mode 100644 index 0000000..73af284 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStatePairVO.java @@ -0,0 +1,72 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public class ClusterUniversalStatePairVO { + + private String ip; + + private Integer commandPort; + + private ClusterUniversalStateVO state; + + public ClusterUniversalStatePairVO() { + } + + public ClusterUniversalStatePairVO(String ip, Integer commandPort, ClusterUniversalStateVO state) { + this.ip = ip; + this.commandPort = commandPort; + this.state = state; + } + + public String getIp() { + return ip; + } + + public ClusterUniversalStatePairVO setIp(String ip) { + this.ip = ip; + return this; + } + + public Integer getCommandPort() { + return commandPort; + } + + public ClusterUniversalStatePairVO setCommandPort(Integer commandPort) { + this.commandPort = commandPort; + return this; + } + + public ClusterUniversalStateVO getState() { + return state; + } + + public ClusterUniversalStatePairVO setState(ClusterUniversalStateVO state) { + this.state = state; + return this; + } + + @Override + public String toString() { + return "ClusterUniversalStatePairVO{" + "ip='" + ip + '\'' + ", commandPort=" + commandPort + ", state=" + state + + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStateVO.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStateVO.java new file mode 100644 index 0000000..75ef4e3 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/cluster/state/ClusterUniversalStateVO.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.cluster.state; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public class ClusterUniversalStateVO { + + private ClusterStateSimpleEntity stateInfo; + + private ClusterClientStateVO client; + + private ClusterServerStateVO server; + + public ClusterClientStateVO getClient() { + return client; + } + + public ClusterUniversalStateVO setClient(ClusterClientStateVO client) { + this.client = client; + return this; + } + + public ClusterServerStateVO getServer() { + return server; + } + + public ClusterUniversalStateVO setServer(ClusterServerStateVO server) { + this.server = server; + return this; + } + + public ClusterStateSimpleEntity getStateInfo() { + return stateInfo; + } + + public ClusterUniversalStateVO setStateInfo(ClusterStateSimpleEntity stateInfo) { + this.stateInfo = stateInfo; + return this; + } + + @Override + public String toString() { + return "ClusterUniversalStateVO{" + "stateInfo=" + stateInfo + ", client=" + client + ", server=" + server + + '}'; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MachineInfoVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MachineInfoVo.java new file mode 100644 index 0000000..65a7fd9 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MachineInfoVo.java @@ -0,0 +1,130 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo; + +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author leyou + */ +public class MachineInfoVo { + + private String app; + + private String hostname; + + private String ip; + + private int port; + + private long heartbeatVersion; + + private long lastHeartbeat; + + private boolean healthy; + + private String version; + + public static List fromMachineInfoList(List machines) { + List list = new ArrayList<>(); + for (MachineInfo machine : machines) { + list.add(fromMachineInfo(machine)); + } + return list; + } + + public static MachineInfoVo fromMachineInfo(MachineInfo machine) { + MachineInfoVo vo = new MachineInfoVo(); + vo.setApp(machine.getApp()); + vo.setHostname(machine.getHostname()); + vo.setIp(machine.getIp()); + vo.setPort(machine.getPort()); + vo.setLastHeartbeat(machine.getLastHeartbeat()); + vo.setHeartbeatVersion(machine.getHeartbeatVersion()); + vo.setVersion(machine.getVersion()); + vo.setHealthy(machine.isHealthy()); + return vo; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public long getLastHeartbeat() { + return lastHeartbeat; + } + + public void setLastHeartbeat(long lastHeartbeat) { + this.lastHeartbeat = lastHeartbeat; + } + + public void setHeartbeatVersion(long heartbeatVersion) { + this.heartbeatVersion = heartbeatVersion; + } + + public long getHeartbeatVersion() { + return heartbeatVersion; + } + + public String getVersion() { + return version; + } + + public MachineInfoVo setVersion(String version) { + this.version = version; + return this; + } + + public boolean isHealthy() { + return healthy; + } + + public void setHealthy(boolean healthy) { + this.healthy = healthy; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MetricVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MetricVo.java new file mode 100644 index 0000000..18e69fc --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/MetricVo.java @@ -0,0 +1,220 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author leyou + */ +public class MetricVo implements Comparable { + + private Long id; + + private String app; + + private Long timestamp; + + private Long gmtCreate = System.currentTimeMillis(); + + private String resource; + + private Long passQps; + + private Long blockQps; + + private Long successQps; + + private Long exceptionQps; + + /** + * average rt + */ + private Double rt; + + private Integer count; + + public MetricVo() { + } + + public static List fromMetricEntities(Collection entities) { + List list = new ArrayList<>(); + if (entities != null) { + for (MetricEntity entity : entities) { + list.add(fromMetricEntity(entity)); + } + } + return list; + } + + /** + * 保留资源名为identity的结果。 + * @param entities 通过hashCode查找到的MetricEntities + * @param identity 真正需要查找的资源名 + * @return + */ + public static List fromMetricEntities(Collection entities, String identity) { + List list = new ArrayList<>(); + if (entities != null) { + for (MetricEntity entity : entities) { + if (entity.getResource().equals(identity)) { + list.add(fromMetricEntity(entity)); + } + } + } + return list; + } + + public static MetricVo fromMetricEntity(MetricEntity entity) { + MetricVo vo = new MetricVo(); + vo.id = entity.getId(); + vo.app = entity.getApp(); + vo.timestamp = entity.getTimestamp(); + vo.gmtCreate = entity.getGmtCreate(); + vo.resource = entity.getResource(); + vo.passQps = entity.getPassQps(); + vo.blockQps = entity.getBlockQps(); + vo.successQps = entity.getSuccessQps(); + vo.exceptionQps = entity.getExceptionQps(); + if (entity.getSuccessQps() != 0) { + vo.rt = entity.getRt() / entity.getSuccessQps(); + } + else { + vo.rt = 0D; + } + vo.count = entity.getCount(); + return vo; + } + + public static MetricVo parse(String line) { + String[] strs = line.split("\\|"); + long timestamp = Long.parseLong(strs[0]); + String identity = strs[1]; + long passQps = Long.parseLong(strs[2]); + long blockQps = Long.parseLong(strs[3]); + long exception = Long.parseLong(strs[4]); + double rt = Double.parseDouble(strs[5]); + long successQps = Long.parseLong(strs[6]); + MetricVo vo = new MetricVo(); + vo.timestamp = timestamp; + vo.resource = identity; + vo.passQps = passQps; + vo.blockQps = blockQps; + vo.successQps = successQps; + vo.exceptionQps = exception; + vo.rt = rt; + vo.count = 1; + return vo; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public Long getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Long gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Long getPassQps() { + return passQps; + } + + public void setPassQps(Long passQps) { + this.passQps = passQps; + } + + public Long getBlockQps() { + return blockQps; + } + + public void setBlockQps(Long blockQps) { + this.blockQps = blockQps; + } + + public Long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(Long successQps) { + this.successQps = successQps; + } + + public Long getExceptionQps() { + return exceptionQps; + } + + public void setExceptionQps(Long exceptionQps) { + this.exceptionQps = exceptionQps; + } + + public Double getRt() { + return rt; + } + + public void setRt(Double rt) { + this.rt = rt; + } + + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + @Override + public int compareTo(MetricVo o) { + return Long.compare(this.timestamp, o.timestamp); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/ResourceVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/ResourceVo.java new file mode 100644 index 0000000..4fd4c2c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/ResourceVo.java @@ -0,0 +1,249 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo; + +import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.dashboard.domain.ResourceTreeNode; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author leyou + */ +public class ResourceVo { + + private String parentTtId; + + private String ttId; + + private String resource; + + private Integer threadNum; + + private Long passQps; + + private Long blockQps; + + private Long totalQps; + + private Long averageRt; + + private Long passRequestQps; + + private Long exceptionQps; + + private Long oneMinutePass; + + private Long oneMinuteBlock; + + private Long oneMinuteException; + + private Long oneMinuteTotal; + + private boolean visible = true; + + public ResourceVo() { + } + + public static List fromNodeVoList(List nodeVos) { + if (nodeVos == null) { + return null; + } + List list = new ArrayList<>(); + for (NodeVo nodeVo : nodeVos) { + ResourceVo vo = new ResourceVo(); + vo.parentTtId = nodeVo.getParentId(); + vo.ttId = nodeVo.getId(); + vo.resource = nodeVo.getResource(); + vo.threadNum = nodeVo.getThreadNum(); + vo.passQps = nodeVo.getPassQps(); + vo.blockQps = nodeVo.getBlockQps(); + vo.totalQps = nodeVo.getTotalQps(); + vo.averageRt = nodeVo.getAverageRt(); + vo.exceptionQps = nodeVo.getExceptionQps(); + vo.oneMinutePass = nodeVo.getOneMinutePass(); + vo.oneMinuteBlock = nodeVo.getOneMinuteBlock(); + vo.oneMinuteException = nodeVo.getOneMinuteException(); + vo.oneMinuteTotal = nodeVo.getOneMinuteTotal(); + list.add(vo); + } + return list; + } + + public static List fromResourceTreeNode(ResourceTreeNode root) { + if (root == null) { + return null; + } + List list = new ArrayList<>(); + visit(root, list, false, true); + // if(!list.isEmpty()){ + // list.remove(0); + // } + return list; + } + + /** + * This node is visible when this.visible==true or one of this's parents is visible, + * root node is always invisible. + */ + private static void visit(ResourceTreeNode node, List list, boolean parentVisible, boolean isRoot) { + boolean visible = !isRoot && (node.isVisible() || parentVisible); + // boolean visible = node.isVisible(); + if (visible) { + ResourceVo vo = new ResourceVo(); + vo.parentTtId = node.getParentId(); + vo.ttId = node.getId(); + vo.resource = node.getResource(); + vo.threadNum = node.getThreadNum(); + vo.passQps = node.getPassQps(); + vo.blockQps = node.getBlockQps(); + vo.totalQps = node.getTotalQps(); + vo.averageRt = node.getAverageRt(); + vo.exceptionQps = node.getExceptionQps(); + vo.oneMinutePass = node.getOneMinutePass(); + vo.oneMinuteBlock = node.getOneMinuteBlock(); + vo.oneMinuteException = node.getOneMinuteException(); + vo.oneMinuteTotal = node.getOneMinuteTotal(); + vo.visible = node.isVisible(); + list.add(vo); + } + for (ResourceTreeNode c : node.getChildren()) { + visit(c, list, visible, false); + } + } + + public String getParentTtId() { + return parentTtId; + } + + public void setParentTtId(String parentTtId) { + this.parentTtId = parentTtId; + } + + public String getTtId() { + return ttId; + } + + public void setTtId(String ttId) { + this.ttId = ttId; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getThreadNum() { + return threadNum; + } + + public void setThreadNum(Integer threadNum) { + this.threadNum = threadNum; + } + + public Long getPassQps() { + return passQps; + } + + public void setPassQps(Long passQps) { + this.passQps = passQps; + } + + public Long getBlockQps() { + return blockQps; + } + + public void setBlockQps(Long blockQps) { + this.blockQps = blockQps; + } + + public Long getTotalQps() { + return totalQps; + } + + public void setTotalQps(Long totalQps) { + this.totalQps = totalQps; + } + + public Long getAverageRt() { + return averageRt; + } + + public void setAverageRt(Long averageRt) { + this.averageRt = averageRt; + } + + public Long getPassRequestQps() { + return passRequestQps; + } + + public void setPassRequestQps(Long passRequestQps) { + this.passRequestQps = passRequestQps; + } + + public Long getExceptionQps() { + return exceptionQps; + } + + public void setExceptionQps(Long exceptionQps) { + this.exceptionQps = exceptionQps; + } + + public Long getOneMinuteException() { + return oneMinuteException; + } + + public void setOneMinuteException(Long oneMinuteException) { + this.oneMinuteException = oneMinuteException; + } + + public Long getOneMinutePass() { + return oneMinutePass; + } + + public void setOneMinutePass(Long oneMinutePass) { + this.oneMinutePass = oneMinutePass; + } + + public Long getOneMinuteBlock() { + return oneMinuteBlock; + } + + public void setOneMinuteBlock(Long oneMinuteBlock) { + this.oneMinuteBlock = oneMinuteBlock; + } + + public Long getOneMinuteTotal() { + return oneMinuteTotal; + } + + public void setOneMinuteTotal(Long oneMinuteTotal) { + this.oneMinuteTotal = oneMinuteTotal; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java new file mode 100644 index 0000000..22888f7 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java @@ -0,0 +1,78 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api; + +import java.util.List; + +/** + * Value Object for add gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class AddApiReqVo { + + private String app; + + private String ip; + + private Integer port; + + private String apiName; + + private List predicateItems; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public List getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(List predicateItems) { + this.predicateItems = predicateItems; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java new file mode 100644 index 0000000..a11cd7c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api; + +/** + * Value Object for add or update gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiPredicateItemVo { + + private String pattern; + + private Integer matchStrategy; + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java new file mode 100644 index 0000000..f41e329 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api; + +import java.util.List; + +/** + * Value Object for update gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class UpdateApiReqVo { + + private Long id; + + private String app; + + private List predicateItems; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public List getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(List predicateItems) { + this.predicateItems = predicateItems; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java new file mode 100644 index 0000000..08ac0c6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java @@ -0,0 +1,156 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for add gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class AddFlowRuleReqVo { + + private String app; + + private String ip; + + private Integer port; + + private String resource; + + private Integer resourceMode; + + private Integer grade; + + private Double count; + + private Long interval; + + private Integer intervalUnit; + + private Integer controlBehavior; + + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemVo paramItem; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getResourceMode() { + return resourceMode; + } + + public void setResourceMode(Integer resourceMode) { + this.resourceMode = resourceMode; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + public GatewayParamFlowItemVo getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemVo paramItem) { + this.paramItem = paramItem; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java new file mode 100644 index 0000000..07c5a91 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java @@ -0,0 +1,66 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for add or update gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayParamFlowItemVo { + + private Integer parseStrategy; + + private String fieldName; + + private String pattern; + + private Integer matchStrategy; + + public Integer getParseStrategy() { + return parseStrategy; + } + + public void setParseStrategy(Integer parseStrategy) { + this.parseStrategy = parseStrategy; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java new file mode 100644 index 0000000..ebc7b10 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java @@ -0,0 +1,126 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for update gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class UpdateFlowRuleReqVo { + + private Long id; + + private String app; + + private Integer grade; + + private Double count; + + private Long interval; + + private Integer intervalUnit; + + private Integer controlBehavior; + + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemVo paramItem; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + public GatewayParamFlowItemVo getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemVo paramItem) { + this.paramItem = paramItem; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/metric/MetricFetcher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/metric/MetricFetcher.java new file mode 100644 index 0000000..af8c865 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/metric/MetricFetcher.java @@ -0,0 +1,382 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.metric; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppInfo; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.repository.metric.MetricsRepository; +import com.alibaba.csp.sentinel.node.metric.MetricNode; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.protocol.HTTP; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.nio.charset.Charset; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Fetch metric of machines. + * + * @author leyou + */ +@Component +public class MetricFetcher { + + public static final String NO_METRICS = "No metrics"; + + private static final int HTTP_OK = 200; + + private static final long MAX_LAST_FETCH_INTERVAL_MS = 1000 * 15; + + private static final long FETCH_INTERVAL_SECOND = 6; + + private static final Charset DEFAULT_CHARSET = Charset.forName(SentinelConfig.charset()); + + private final static String METRIC_URL_PATH = "metric"; + + private static Logger logger = LoggerFactory.getLogger(MetricFetcher.class); + + private final long intervalSecond = 1; + + private Map appLastFetchTime = new ConcurrentHashMap<>(); + + @Autowired + private MetricsRepository metricStore; + + @Autowired + private AppManagement appManagement; + + private CloseableHttpAsyncClient httpclient; + + @SuppressWarnings("PMD.ThreadPoolCreationRule") + private ScheduledExecutorService fetchScheduleService = Executors.newScheduledThreadPool(1, + new NamedThreadFactory("sentinel-dashboard-metrics-fetch-task")); + + private ExecutorService fetchService; + + private ExecutorService fetchWorker; + + public MetricFetcher() { + int cores = Runtime.getRuntime().availableProcessors() * 2; + long keepAliveTime = 0; + int queueSize = 2048; + RejectedExecutionHandler handler = new DiscardPolicy(); + fetchService = new ThreadPoolExecutor(cores, cores, keepAliveTime, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(queueSize), new NamedThreadFactory("sentinel-dashboard-metrics-fetchService"), + handler); + fetchWorker = new ThreadPoolExecutor(cores, cores, keepAliveTime, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(queueSize), new NamedThreadFactory("sentinel-dashboard-metrics-fetchWorker"), + handler); + IOReactorConfig ioConfig = IOReactorConfig.custom().setConnectTimeout(3000).setSoTimeout(3000) + .setIoThreadCount(Runtime.getRuntime().availableProcessors() * 2).build(); + + httpclient = HttpAsyncClients.custom().setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(final String method) { + return false; + } + }).setMaxConnTotal(4000).setMaxConnPerRoute(1000).setDefaultIOReactorConfig(ioConfig).build(); + httpclient.start(); + start(); + } + + private void start() { + fetchScheduleService.scheduleAtFixedRate(() -> { + try { + fetchAllApp(); + } + catch (Exception e) { + logger.info("fetchAllApp error:", e); + } + }, 10, intervalSecond, TimeUnit.SECONDS); + } + + private void writeMetric(Map map) { + if (map.isEmpty()) { + return; + } + long date = System.currentTimeMillis(); + for (MetricEntity entity : map.values()) { + entity.setGmtCreate(date); + entity.setGmtModified(date); + } + metricStore.saveAll(map.values()); + } + + /** + * Traverse each APP, and then pull the metric of all machines for that APP. + */ + private void fetchAllApp() { + List apps = appManagement.getAppNames(); + if (apps == null) { + return; + } + for (final String app : apps) { + fetchService.submit(() -> { + try { + doFetchAppMetric(app); + } + catch (Exception e) { + logger.error("fetchAppMetric error", e); + } + }); + } + } + + /** + * fetch metric between [startTime, endTime], both side inclusive + */ + private void fetchOnce(String app, long startTime, long endTime, int maxWaitSeconds) { + if (maxWaitSeconds <= 0) { + throw new IllegalArgumentException("maxWaitSeconds must > 0, but " + maxWaitSeconds); + } + AppInfo appInfo = appManagement.getDetailApp(app); + // auto remove for app + if (appInfo.isDead()) { + logger.info("Dead app removed: {}", app); + appManagement.removeApp(app); + return; + } + Set machines = appInfo.getMachines(); + logger.debug("enter fetchOnce(" + app + "), machines.size()=" + machines.size() + ", time intervalMs [" + + startTime + ", " + endTime + "]"); + if (machines.isEmpty()) { + return; + } + final String msg = "fetch"; + AtomicLong unhealthy = new AtomicLong(); + final AtomicLong success = new AtomicLong(); + final AtomicLong fail = new AtomicLong(); + + long start = System.currentTimeMillis(); + /** app_resource_timeSecond -> metric */ + final Map metricMap = new ConcurrentHashMap<>(16); + final CountDownLatch latch = new CountDownLatch(machines.size()); + for (final MachineInfo machine : machines) { + // auto remove + if (machine.isDead()) { + latch.countDown(); + appManagement.getDetailApp(app).removeMachine(machine.getIp(), machine.getPort()); + logger.info("Dead machine removed: {}:{} of {}", machine.getIp(), machine.getPort(), app); + continue; + } + if (!machine.isHealthy()) { + latch.countDown(); + unhealthy.incrementAndGet(); + continue; + } + final String url = "http://" + machine.getIp() + ":" + machine.getPort() + "/" + METRIC_URL_PATH + + "?startTime=" + startTime + "&endTime=" + endTime + "&refetch=" + false; + final HttpGet httpGet = new HttpGet(url); + httpGet.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE); + httpclient.execute(httpGet, new FutureCallback() { + @Override + public void completed(final HttpResponse response) { + try { + handleResponse(response, machine, metricMap); + success.incrementAndGet(); + } + catch (Exception e) { + logger.error(msg + " metric " + url + " error:", e); + } + finally { + latch.countDown(); + } + } + + @Override + public void failed(final Exception ex) { + latch.countDown(); + fail.incrementAndGet(); + httpGet.abort(); + if (ex instanceof SocketTimeoutException) { + logger.error("Failed to fetch metric from <{}>: socket timeout", url); + } + else if (ex instanceof ConnectException) { + logger.error("Failed to fetch metric from <{}> (ConnectionException: {})", url, + ex.getMessage()); + } + else { + logger.error(msg + " metric " + url + " error", ex); + } + } + + @Override + public void cancelled() { + latch.countDown(); + fail.incrementAndGet(); + httpGet.abort(); + } + }); + } + try { + latch.await(maxWaitSeconds, TimeUnit.SECONDS); + } + catch (Exception e) { + logger.info(msg + " metric, wait http client error:", e); + } + long cost = System.currentTimeMillis() - start; + // logger.info("finished " + msg + " metric for " + app + ", time intervalMs [" + + // startTime + ", " + endTime + // + "], total machines=" + machines.size() + ", dead=" + dead + ", fetch + // success=" + // + success + ", fetch fail=" + fail + ", time cost=" + cost + " ms"); + writeMetric(metricMap); + } + + private void doFetchAppMetric(final String app) { + long now = System.currentTimeMillis(); + long lastFetchMs = now - MAX_LAST_FETCH_INTERVAL_MS; + if (appLastFetchTime.containsKey(app)) { + lastFetchMs = Math.max(lastFetchMs, appLastFetchTime.get(app).get() + 1000); + } + // trim milliseconds + lastFetchMs = lastFetchMs / 1000 * 1000; + long endTime = lastFetchMs + FETCH_INTERVAL_SECOND * 1000; + if (endTime > now - 1000 * 2) { + // to near + return; + } + // update last_fetch in advance. + appLastFetchTime.computeIfAbsent(app, a -> new AtomicLong()).set(endTime); + final long finalLastFetchMs = lastFetchMs; + final long finalEndTime = endTime; + try { + // do real fetch async + fetchWorker.submit(() -> { + try { + fetchOnce(app, finalLastFetchMs, finalEndTime, 5); + } + catch (Exception e) { + logger.info("fetchOnce(" + app + ") error", e); + } + }); + } + catch (Exception e) { + logger.info("submit fetchOnce(" + app + ") fail, intervalMs [" + lastFetchMs + ", " + endTime + "]", e); + } + } + + private void handleResponse(final HttpResponse response, MachineInfo machine, Map metricMap) + throws Exception { + int code = response.getStatusLine().getStatusCode(); + if (code != HTTP_OK) { + return; + } + Charset charset = null; + try { + String contentTypeStr = response.getFirstHeader("Content-type").getValue(); + if (StringUtil.isNotEmpty(contentTypeStr)) { + ContentType contentType = ContentType.parse(contentTypeStr); + charset = contentType.getCharset(); + } + } + catch (Exception ignore) { + } + String body = EntityUtils.toString(response.getEntity(), charset != null ? charset : DEFAULT_CHARSET); + if (StringUtil.isEmpty(body) || body.startsWith(NO_METRICS)) { + // logger.info(machine.getApp() + ":" + machine.getIp() + ":" + + // machine.getPort() + ", bodyStr is empty"); + return; + } + String[] lines = body.split("\n"); + // logger.info(machine.getApp() + ":" + machine.getIp() + ":" + machine.getPort() + // + + // ", bodyStr.length()=" + body.length() + ", lines=" + lines.length); + handleBody(lines, machine, metricMap); + } + + private void handleBody(String[] lines, MachineInfo machine, Map map) { + // logger.info("handleBody() lines=" + lines.length + ", machine=" + machine); + if (lines.length < 1) { + return; + } + + for (String line : lines) { + try { + MetricNode node = MetricNode.fromThinString(line); + if (shouldFilterOut(node.getResource())) { + continue; + } + /* + * aggregation metrics by app_resource_timeSecond, ignore ip and port. + */ + String key = buildMetricKey(machine.getApp(), node.getResource(), node.getTimestamp()); + MetricEntity entity = map.get(key); + if (entity != null) { + entity.addPassQps(node.getPassQps()); + entity.addBlockQps(node.getBlockQps()); + entity.addRtAndSuccessQps(node.getRt(), node.getSuccessQps()); + entity.addExceptionQps(node.getExceptionQps()); + entity.addCount(1); + } + else { + entity = new MetricEntity(); + entity.setApp(machine.getApp()); + entity.setTimestamp(node.getTimestamp()); + entity.setPassQps(node.getPassQps()); + entity.setBlockQps(node.getBlockQps()); + entity.setRtAndSuccessQps(node.getRt(), node.getSuccessQps()); + entity.setExceptionQps(node.getExceptionQps()); + entity.setCount(1); + entity.setResource(node.getResource()); + map.put(key, entity); + } + } + catch (Exception e) { + logger.warn("handleBody line exception, machine: {}, line: {}", machine.toLogString(), line); + } + } + } + + private String buildMetricKey(String app, String resource, long timestamp) { + return app + "__" + resource + "__" + (timestamp / 1000); + } + + private boolean shouldFilterOut(String resource) { + return RES_EXCLUSION_SET.contains(resource); + } + + private static final Set RES_EXCLUSION_SET = new HashSet() { + { + add(Constants.TOTAL_IN_RESOURCE_NAME); + add(Constants.SYSTEM_LOAD_RESOURCE_NAME); + add(Constants.CPU_USAGE_RESOURCE_NAME); + } + }; + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java new file mode 100644 index 0000000..2e516d8 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java @@ -0,0 +1,40 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.gateway; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Store {@link ApiDefinitionEntity} in memory. + * + * @author cdfive + * @since 1.7.0 + */ +@Component +public class InMemApiDefinitionStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java new file mode 100644 index 0000000..a793ef6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java @@ -0,0 +1,40 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.gateway; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Store {@link GatewayFlowRuleEntity} in memory. + * + * @author cdfive + * @since 1.7.0 + */ +@Component +public class InMemGatewayFlowRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/InMemoryMetricsRepository.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/InMemoryMetricsRepository.java new file mode 100644 index 0000000..5be3f56 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/InMemoryMetricsRepository.java @@ -0,0 +1,177 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.metric; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity; +import com.alibaba.csp.sentinel.dashboard.util.pool.PoolUtils; +import com.alibaba.csp.sentinel.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +/** + * Caches metrics data in a period of time in memory. + * + * @author Carpenter Lee + * @author Eric Zhao + */ +@Slf4j +@Component +public class InMemoryMetricsRepository implements MetricsRepository { + + private static final Class clazz = MetricEntity.class; + + @Autowired + MongoTemplate mongoTemplate; + + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + + @Override + public void save(MetricEntity entity) { + if (entity == null || StringUtil.isBlank(entity.getApp())) { + return; + } + readWriteLock.writeLock().lock(); + try { + PoolUtils.SENTINEL.getInstance().execute(()->mongoTemplate.save(entity)); + } + finally { + readWriteLock.writeLock().unlock(); + + PoolUtils.SENTINEL.getInstance().execute(this::remove); + } + + } + + @Override + public void saveAll(Iterable metrics) { + if (metrics == null) { + return; + } + readWriteLock.writeLock().lock(); + try { + PoolUtils.SENTINEL.getInstance().execute(()->{ + List list = new ArrayList<>(); + metrics.forEach(list::add); + mongoTemplate.insertAll(list); + }); + } + finally { + readWriteLock.writeLock().unlock(); + + PoolUtils.SENTINEL.getInstance().execute(this::remove); + } + } + + @Override + public List queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) { + List results = new ArrayList<>(); + if (StringUtil.isBlank(app)) { + return results; + } + + readWriteLock.readLock().lock(); + try { + List metricList = mongoTemplate.find(new Query( + Criteria.where("app").is(app) + .andOperator( + List.of(Criteria.where("timestamp").gte(startTime), + Criteria.where("timestamp").lte(endTime)) + ) + ), clazz); + if (CollectionUtils.isEmpty(metricList)){ + return results; + } + results.addAll(metricList); + return results; + } + finally { + readWriteLock.readLock().unlock(); + } + } + + @Override + public List listResourcesOfApp(String app) { + List results = new ArrayList<>(); + if (StringUtil.isBlank(app)) { + return results; + } + readWriteLock.readLock().lock(); + try { + final long minTimeMs = System.currentTimeMillis() - 1000 * 60 * 60; + Map resourceCount = new ConcurrentHashMap<>(32); + List entityList = mongoTemplate.find(new Query( + Criteria.where("app").is(app) + .and("timestamp").gte(minTimeMs)), clazz); + if (CollectionUtils.isEmpty(entityList)){ + return results; + } + for (MetricEntity newEntity : entityList) { + String resource = newEntity.getResource(); + if (resourceCount.containsKey(resource)) { + MetricEntity oldEntity = resourceCount.get(resource); + oldEntity.addPassQps(newEntity.getPassQps()); + oldEntity.addRtAndSuccessQps(newEntity.getRt(), newEntity.getSuccessQps()); + oldEntity.addBlockQps(newEntity.getBlockQps()); + oldEntity.addExceptionQps(newEntity.getExceptionQps()); + oldEntity.addCount(1); + } else { + resourceCount.put(resource, MetricEntity.copyOf(newEntity)); + } + } + // Order by last minute b_qps DESC. + return resourceCount.entrySet() + .stream() + .sorted((o1, o2) -> { + MetricEntity e1 = o1.getValue(); + MetricEntity e2 = o2.getValue(); + int t = e2.getBlockQps().compareTo(e1.getBlockQps()); + if (t != 0) { + return t; + } + return e2.getPassQps().compareTo(e1.getPassQps()); + }).map(Entry::getKey).collect(Collectors.toList()); + } + finally { + readWriteLock.readLock().unlock(); + } + } + + public void remove() { + long count = mongoTemplate.count(new Query(), MetricEntity.class); + // 每30万数据进行一次清库 + if (count > 300000){ + long currentTimeMillis = System.currentTimeMillis(); + log.info("清除 mongo sentinel 数据 {}, 30秒之前的所有数据", currentTimeMillis); + mongoTemplate.remove(new Query() + .addCriteria(Criteria.where("timestamp").lt(currentTimeMillis - 30000)), MetricEntity.class); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/MetricsRepository.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/MetricsRepository.java new file mode 100644 index 0000000..6676145 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/metric/MetricsRepository.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.metric; + +import java.util.List; + +/** + * Repository interface for aggregated metrics data. + * + * @param type of metrics + * @author Eric Zhao + */ +public interface MetricsRepository { + + /** + * Save the metric to the storage repository. + * @param metric metric data to save + */ + void save(T metric); + + /** + * Save all metrics to the storage repository. + * @param metrics metrics to save + */ + void saveAll(Iterable metrics); + + /** + * Get all metrics by {@code appName} and {@code resourceName} between a period of + * time. + * @param app application name for Sentinel + * @param resource resource name + * @param startTime start timestamp + * @param endTime end timestamp + * @return all metrics in query conditions + */ + List queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime); + + /** + * List resource name of provided application name. + * @param app application name + * @return list of resources + */ + List listResourcesOfApp(String app); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemAuthorityRuleStore.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemAuthorityRuleStore.java new file mode 100644 index 0000000..422e0e6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemAuthorityRuleStore.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.rule; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * In-memory storage for authority rules. + * + * @author Eric Zhao + * @since 0.2.1 + */ +@Component +public class InMemAuthorityRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemDegradeRuleStore.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemDegradeRuleStore.java new file mode 100644 index 0000000..7891924 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemDegradeRuleStore.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.rule; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author leyou + */ +@Component +public class InMemDegradeRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemFlowRuleStore.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemFlowRuleStore.java new file mode 100644 index 0000000..c54fdb2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemFlowRuleStore.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.rule; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.slots.block.flow.ClusterFlowConfig; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Store {@link FlowRuleEntity} in memory. + * + * @author leyou + */ +@Component +public class InMemFlowRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } + + @Override + protected FlowRuleEntity preProcess(FlowRuleEntity entity) { + if (entity != null && entity.isClusterMode()) { + ClusterFlowConfig config = entity.getClusterConfig(); + if (config == null) { + config = new ClusterFlowConfig(); + entity.setClusterConfig(config); + } + // Set cluster rule id. + config.setFlowId(entity.getId()); + } + return entity; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemParamFlowRuleStore.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemParamFlowRuleStore.java new file mode 100644 index 0000000..6ca2040 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemParamFlowRuleStore.java @@ -0,0 +1,51 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.rule; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowClusterConfig; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Eric Zhao + * @since 0.2.1 + */ +@Component +public class InMemParamFlowRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } + + @Override + protected ParamFlowRuleEntity preProcess(ParamFlowRuleEntity entity) { + if (entity != null && entity.isClusterMode()) { + ParamFlowClusterConfig config = entity.getClusterConfig(); + if (config == null) { + config = new ParamFlowClusterConfig(); + } + // Set cluster rule id. + config.setFlowId(entity.getId()); + } + return entity; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemSystemRuleStore.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemSystemRuleStore.java new file mode 100644 index 0000000..7a4e943 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemSystemRuleStore.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.rule; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.SystemRuleEntity; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author leyou + */ +@Component +public class InMemSystemRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java new file mode 100644 index 0000000..2e6e15c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java @@ -0,0 +1,131 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.rule; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.util.AssertUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author leyou + */ +public abstract class InMemoryRuleRepositoryAdapter implements RuleRepository { + + /** + * {@code >} + */ + private Map> machineRules = new ConcurrentHashMap<>(16); + + private Map allRules = new ConcurrentHashMap<>(16); + + private Map> appRules = new ConcurrentHashMap<>(16); + + private static final int MAX_RULES_SIZE = 10000; + + @Override + public T save(T entity) { + if (entity.getId() == null) { + entity.setId(nextId()); + } + T processedEntity = preProcess(entity); + if (processedEntity != null) { + allRules.put(processedEntity.getId(), processedEntity); + machineRules + .computeIfAbsent(MachineInfo.of(processedEntity.getApp(), processedEntity.getIp(), + processedEntity.getPort()), e -> new ConcurrentHashMap<>(32)) + .put(processedEntity.getId(), processedEntity); + appRules.computeIfAbsent(processedEntity.getApp(), v -> new ConcurrentHashMap<>(32)) + .put(processedEntity.getId(), processedEntity); + } + + return processedEntity; + } + + @Override + public List saveAll(List rules) { + // TODO: check here. + allRules.clear(); + machineRules.clear(); + appRules.clear(); + + if (rules == null) { + return null; + } + List savedRules = new ArrayList<>(rules.size()); + for (T rule : rules) { + savedRules.add(save(rule)); + } + return savedRules; + } + + @Override + public T delete(Long id) { + T entity = allRules.remove(id); + if (entity != null) { + if (appRules.get(entity.getApp()) != null) { + appRules.get(entity.getApp()).remove(id); + } + machineRules.get(MachineInfo.of(entity.getApp(), entity.getIp(), entity.getPort())).remove(id); + } + return entity; + } + + @Override + public T findById(Long id) { + return allRules.get(id); + } + + @Override + public List findAllByMachine(MachineInfo machineInfo) { + Map entities = machineRules.get(machineInfo); + if (entities == null) { + return new ArrayList<>(); + } + return new ArrayList<>(entities.values()); + } + + @Override + public List findAllByApp(String appName) { + AssertUtil.notEmpty(appName, "appName cannot be empty"); + Map entities = appRules.get(appName); + if (entities == null) { + return new ArrayList<>(); + } + return new ArrayList<>(entities.values()); + } + + public void clearAll() { + allRules.clear(); + machineRules.clear(); + appRules.clear(); + } + + protected T preProcess(T entity) { + return entity; + } + + /** + * Get next unused id. + * @return next unused id + */ + abstract protected long nextId(); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/RuleRepository.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/RuleRepository.java new file mode 100644 index 0000000..26f19d4 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/RuleRepository.java @@ -0,0 +1,80 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.repository.rule; + +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; + +import java.util.List; + +/** + * Interface to store and find rules. + * + * @author leyou + */ +public interface RuleRepository { + + /** + * Save one. + * @param entity + * @return + */ + T save(T entity); + + /** + * Save all. + * @param rules + * @return rules saved. + */ + List saveAll(List rules); + + /** + * Delete by id + * @param id + * @return entity deleted + */ + T delete(ID id); + + /** + * Find by id. + * @param id + * @return + */ + T findById(ID id); + + /** + * Find all by machine. + * @param machineInfo + * @return + */ + List findAllByMachine(MachineInfo machineInfo); + + /** + * Find all by application. + * @param appName valid app name + * @return all rules of the application + * @since 1.4.0 + */ + List findAllByApp(String appName); + + /// ** + // * Find all by app and enable switch. + // * @param app + // * @param enable + // * @return + // */ + // List findAllByAppAndEnable(String app, boolean enable); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRuleProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRuleProvider.java new file mode 100644 index 0000000..1154495 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRuleProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public interface DynamicRuleProvider { + + T getRules(String appName) throws Exception; + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRulePublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRulePublisher.java new file mode 100644 index 0000000..7550a3e --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/DynamicRulePublisher.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public interface DynamicRulePublisher { + + /** + * Publish rules to remote rule configuration center for given application name. + * @param appName 应用名称 + * @param rules list of rules to push + * @throws Exception if some error occurs + */ + void publish(String appName, T rules) throws Exception; + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiProvider.java new file mode 100644 index 0000000..c5bf9fc --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule; + +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Eric Zhao + */ +@Component("flowRuleDefaultProvider") +public class FlowRuleApiProvider implements DynamicRuleProvider> { + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private AppManagement appManagement; + + @Override + public List getRules(String appName) throws Exception { + if (StringUtil.isBlank(appName)) { + return new ArrayList<>(); + } + List list = appManagement.getDetailApp(appName).getMachines().stream() + .filter(MachineInfo::isHealthy) + .sorted((e1, e2) -> Long.compare(e2.getLastHeartbeat(), e1.getLastHeartbeat())) + .collect(Collectors.toList()); + if (list.isEmpty()) { + return new ArrayList<>(); + } + else { + MachineInfo machine = list.get(0); + return sentinelApiClient.fetchFlowRuleOfMachine(machine.getApp(), machine.getIp(), machine.getPort()); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiPublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiPublisher.java new file mode 100644 index 0000000..a5d8ac8 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/FlowRuleApiPublisher.java @@ -0,0 +1,61 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule; + +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("flowRuleDefaultPublisher") +public class FlowRuleApiPublisher implements DynamicRulePublisher> { + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private AppManagement appManagement; + + @Override + public void publish(String app, List rules) throws Exception { + if (StringUtil.isBlank(app)) { + return; + } + if (rules == null) { + return; + } + Set set = appManagement.getDetailApp(app).getMachines(); + + for (MachineInfo machine : set) { + if (!machine.isHealthy()) { + continue; + } + // TODO: parse the results + sentinelApiClient.setFlowRuleOfMachine(app, machine.getIp(), machine.getPort(), rules); + } + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosProvider.java new file mode 100644 index 0000000..2520e7d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("authRuleNacosProvider") +public class AuthRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + NacosConfigUtil.AUTH_POSTFIX, + NacosConfigUtil.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosPublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosPublisher.java new file mode 100644 index 0000000..2c4f69d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/AuthRuleNacosPublisher.java @@ -0,0 +1,49 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("authRuleNacosPublisher") +public class AuthRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String appName, List rules) throws Exception { + AssertUtil.notEmpty(appName, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(appName + NacosConfigUtil.AUTH_POSTFIX, + NacosConfigUtil.GROUP_ID, converter.convert(rules)); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosProvider.java new file mode 100644 index 0000000..cc352e3 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("degradeNacosProvider") +public class DegradeRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + NacosConfigUtil.DEGRADE_POSTFIX, + NacosConfigUtil.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosPublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosPublisher.java new file mode 100644 index 0000000..486b2ae --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/DegradeRuleNacosPublisher.java @@ -0,0 +1,49 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("degradeNacosPublisher") +public class DegradeRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String appName, List rules) throws Exception { + AssertUtil.notEmpty(appName, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(appName + NacosConfigUtil.DEGRADE_POSTFIX, + NacosConfigUtil.GROUP_ID, converter.convert(rules)); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosProvider.java new file mode 100644 index 0000000..91adcaa --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("flowRuleNacosProvider") +public class FlowRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX, + NacosConfigUtil.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosPublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosPublisher.java new file mode 100644 index 0000000..e01d0c4 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosPublisher.java @@ -0,0 +1,49 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("flowRuleNacosPublisher") +public class FlowRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String appName, List rules) throws Exception { + AssertUtil.notEmpty(appName, "dataId name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX, + NacosConfigUtil.GROUP_ID, converter.convert(rules)); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosProvider.java new file mode 100644 index 0000000..be8888c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("gatewayApiNacosProvider") +public class GatewayApiNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + NacosConfigUtil.GATEWAY_API_POSTFIX, + NacosConfigUtil.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosPublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosPublisher.java new file mode 100644 index 0000000..136aac5 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayApiNacosPublisher.java @@ -0,0 +1,33 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * by zhaowenyuan create 2022/3/14 09:47 + */ +@Component("gatewayApiNacosPublisher") +public class GatewayApiNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String appName, List rules) throws Exception { + AssertUtil.notEmpty(appName, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(appName + NacosConfigUtil.GATEWAY_API_POSTFIX, + NacosConfigUtil.GROUP_ID, converter.convert(rules)); + } +} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosProvider.java new file mode 100644 index 0000000..08be33b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("gatewayGlowRuleNacosProvider") +public class GatewayFlowRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + NacosConfigUtil.GATEWAY_API_FLOW_POSTFIX, + NacosConfigUtil.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosPublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosPublisher.java new file mode 100644 index 0000000..aa28a35 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/GatewayFlowRuleNacosPublisher.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("gatewayGlowRuleNacosPublisher") +public class GatewayFlowRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String appName, List rules) throws Exception { + AssertUtil.notEmpty(appName, "dataId name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(appName + NacosConfigUtil.GATEWAY_API_FLOW_POSTFIX, + NacosConfigUtil.GROUP_ID, converter.convert(rules)); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfig.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfig.java new file mode 100644 index 0000000..bc5b726 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfig.java @@ -0,0 +1,125 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.*; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.api.PropertyKeyConst; +import com.alibaba.nacos.api.config.ConfigFactory; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Properties; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Configuration +public class NacosConfig { + + @Value("${spring.cloud.nacos.discovery.server-addr}") + private String serverAddr; + @Value("${spring.cloud.nacos.discovery.namespace}") + private String namespace; + + /** + * 流控 规则 + */ + @Bean + public Converter, String> flowRuleEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> flowRuleEntityDecoder() { + return s -> JSON.parseArray(s, FlowRuleEntity.class); + } + + /** + * 降级规则 + */ + @Bean + public Converter, String> degradeRuleEntityEncoder() { + return JSON::toJSONString; + } + @Bean + public Converter> degradeRuleEntityDecoder() { + return s -> JSON.parseArray(s, DegradeRuleEntity.class); + } + + /** + * 热点参数 规则 + */ + @Bean + public Converter, String> paramRuleEntityEncoder() { + return JSON::toJSONString; + } + @Bean + public Converter> paramRuleEntityDecoder() { + return s -> JSON.parseArray(s, ParamFlowRuleEntity.class); + } + + /** + * 授权规则 + */ + @Bean + public Converter, String> authRuleEntityEncoder() { + return JSON::toJSONString; + } + @Bean + public Converter> authRuleEntityDecoder() { + return s -> JSON.parseArray(s, AuthorityRuleEntity.class); + } + + /** + * 网关API + */ + @Bean + public Converter,String> apiDefinitionEntityEncoder() { + return JSON::toJSONString; + } + @Bean + public Converter> apiDefinitionEntityDecoder(){ + return s -> JSON.parseArray(s,ApiDefinitionEntity.class); + } + + /** + * 网关flowRule + */ + @Bean + public Converter,String> gatewayFlowRuleEntityEncoder() { + return JSON::toJSONString; + } + @Bean + public Converter> gatewayFlowRuleEntityDecoder(){ + return s -> JSON.parseArray(s,GatewayFlowRuleEntity.class); + } + + @Bean + public ConfigService nacosConfigService() throws Exception { + Properties properties = new Properties(); + properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr); + properties.put(PropertyKeyConst.NAMESPACE, namespace); + return ConfigFactory.createConfigService(properties); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigUtil.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigUtil.java new file mode 100644 index 0000000..0cadf69 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +public interface NacosConfigUtil { + + String GROUP_ID = "SENTINEL_GROUP"; + + String FLOW_DATA_ID_POSTFIX = "-flow-rules"; + String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules"; + + String GATEWAY_API_POSTFIX = "-gateway-api"; + String GATEWAY_API_FLOW_POSTFIX = "-gateway-flow-rules"; + + String DEGRADE_POSTFIX = "-degrade-rules"; + + String AUTH_POSTFIX = "-auth-rules"; + + String CLUSTER_MAP_DATA_ID_POSTFIX = "-cluster-map"; + + /** + * cc for `cluster-client` + */ + String CLIENT_CONFIG_DATA_ID_POSTFIX = "-cc-config"; + /** + * cs for `cluster-server` + */ + String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX = "-cs-transport-config"; + String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX = "-cs-flow-config"; + String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX = "-cs-namespace-set"; +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosProvider.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosProvider.java new file mode 100644 index 0000000..784364e --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("paramFlowRuleNacosProvider") +public class ParamFlowRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + NacosConfigUtil.PARAM_FLOW_DATA_ID_POSTFIX, + NacosConfigUtil.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosPublisher.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosPublisher.java new file mode 100644 index 0000000..7a16150 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/ParamFlowRuleNacosPublisher.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Component("paramFlowRuleNacosPublisher") +public class ParamFlowRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String appName, List rules) throws Exception { + AssertUtil.notEmpty(appName, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(appName + NacosConfigUtil.PARAM_FLOW_DATA_ID_POSTFIX, + NacosConfigUtil.GROUP_ID, converter.convert(rules)); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignService.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignService.java new file mode 100644 index 0000000..26be98d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignService.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.service; + +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterAppAssignResultVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterAppAssignMap; + +import java.util.List; +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public interface ClusterAssignService { + + /** + * Unbind a specific cluster server and its clients. + * @param app app name + * @param machineId valid machine ID ({@code host@commandPort}) + * @return assign result + */ + ClusterAppAssignResultVO unbindClusterServer(String app, String machineId); + + /** + * Unbind a set of cluster servers and its clients. + * @param app app name + * @param machineIdSet set of valid machine ID ({@code host@commandPort}) + * @return assign result + */ + ClusterAppAssignResultVO unbindClusterServers(String app, Set machineIdSet); + + /** + * Apply cluster server and client assignment for provided app. + * @param app app name + * @param clusterMap cluster assign map (server -> clients) + * @param remainingSet unassigned set of machine ID + * @return assign result + */ + ClusterAppAssignResultVO applyAssignToApp(String app, List clusterMap, + Set remainingSet); + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignServiceImpl.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignServiceImpl.java new file mode 100644 index 0000000..1635e8d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterAssignServiceImpl.java @@ -0,0 +1,241 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.service; + +import com.alibaba.csp.sentinel.cluster.ClusterStateManager; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterAppAssignResultVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterGroupEntity; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ClusterClientConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerFlowConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerTransportConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterAppAssignMap; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterUniversalStatePairVO; +import com.alibaba.csp.sentinel.dashboard.util.MachineUtils; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.function.Tuple2; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +@Service +public class ClusterAssignServiceImpl implements ClusterAssignService { + + private final Logger LOGGER = LoggerFactory.getLogger(ClusterAssignServiceImpl.class); + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private ClusterConfigService clusterConfigService; + + private boolean isMachineInApp(/* @NonEmpty */ String machineId) { + return machineId.contains(":"); + } + + private ClusterAppAssignResultVO handleUnbindClusterServerNotInApp(String app, String machineId) { + Set failedSet = new HashSet<>(); + try { + List list = clusterConfigService.getClusterUniversalState(app).get(10, + TimeUnit.SECONDS); + Set toModifySet = list.stream() + .filter(e -> e.getState().getStateInfo().getMode() == ClusterStateManager.CLUSTER_CLIENT) + .filter(e -> machineId.equals(e.getState().getClient().getClientConfig().getServerHost() + ':' + + e.getState().getClient().getClientConfig().getServerPort())) + .map(e -> e.getIp() + '@' + e.getCommandPort()).collect(Collectors.toSet()); + // Modify mode to NOT-STARTED for all associated token clients. + modifyToNonStarted(toModifySet, failedSet); + } + catch (Exception ex) { + Throwable e = ex instanceof ExecutionException ? ex.getCause() : ex; + LOGGER.error("Failed to unbind machine <{}>", machineId, e); + failedSet.add(machineId); + } + return new ClusterAppAssignResultVO().setFailedClientSet(failedSet).setFailedServerSet(new HashSet<>()); + } + + private void modifyToNonStarted(Set toModifySet, Set failedSet) { + toModifySet.parallelStream().map(MachineUtils::parseCommandIpAndPort).filter(Optional::isPresent) + .map(Optional::get).map(e -> { + CompletableFuture f = modifyMode(e.r1, e.r2, ClusterStateManager.CLUSTER_NOT_STARTED); + return Tuple2.of(e.r1 + '@' + e.r2, f); + }).forEach(f -> handleFutureSync(f, failedSet)); + } + + @Override + public ClusterAppAssignResultVO unbindClusterServer(String app, String machineId) { + AssertUtil.assertNotBlank(app, "app cannot be blank"); + AssertUtil.assertNotBlank(machineId, "machineId cannot be blank"); + + if (isMachineInApp(machineId)) { + return handleUnbindClusterServerNotInApp(app, machineId); + } + Set failedSet = new HashSet<>(); + try { + ClusterGroupEntity entity = clusterConfigService.getClusterUniversalStateForAppMachine(app, machineId) + .get(10, TimeUnit.SECONDS); + Set toModifySet = new HashSet<>(); + toModifySet.add(machineId); + if (entity.getClientSet() != null) { + toModifySet.addAll(entity.getClientSet()); + } + // Modify mode to NOT-STARTED for all chosen token servers and associated + // token clients. + modifyToNonStarted(toModifySet, failedSet); + } + catch (Exception ex) { + Throwable e = ex instanceof ExecutionException ? ex.getCause() : ex; + LOGGER.error("Failed to unbind machine <{}>", machineId, e); + failedSet.add(machineId); + } + return new ClusterAppAssignResultVO().setFailedClientSet(failedSet).setFailedServerSet(new HashSet<>()); + } + + @Override + public ClusterAppAssignResultVO unbindClusterServers(String app, Set machineIdSet) { + AssertUtil.assertNotBlank(app, "app cannot be blank"); + AssertUtil.isTrue(machineIdSet != null && !machineIdSet.isEmpty(), "machineIdSet cannot be empty"); + ClusterAppAssignResultVO result = new ClusterAppAssignResultVO().setFailedClientSet(new HashSet<>()) + .setFailedServerSet(new HashSet<>()); + for (String machineId : machineIdSet) { + ClusterAppAssignResultVO resultVO = unbindClusterServer(app, machineId); + result.getFailedClientSet().addAll(resultVO.getFailedClientSet()); + result.getFailedServerSet().addAll(resultVO.getFailedServerSet()); + } + return result; + } + + @Override + public ClusterAppAssignResultVO applyAssignToApp(String app, List clusterMap, + Set remainingSet) { + AssertUtil.assertNotBlank(app, "app cannot be blank"); + AssertUtil.notNull(clusterMap, "clusterMap cannot be null"); + Set failedServerSet = new HashSet<>(); + Set failedClientSet = new HashSet<>(); + + // Assign server and apply config. + clusterMap.stream().filter(Objects::nonNull).filter(ClusterAppAssignMap::getBelongToApp).map(e -> { + String ip = e.getIp(); + int commandPort = parsePort(e); + CompletableFuture f = modifyMode(ip, commandPort, ClusterStateManager.CLUSTER_SERVER) + .thenCompose(v -> applyServerConfigChange(app, ip, commandPort, e)); + return Tuple2.of(e.getMachineId(), f); + }).forEach(t -> handleFutureSync(t, failedServerSet)); + + // Assign client of servers and apply config. + clusterMap.parallelStream().filter(Objects::nonNull) + .forEach(e -> applyAllClientConfigChange(app, e, failedClientSet)); + + // Unbind remaining (unassigned) machines. + applyAllRemainingMachineSet(app, remainingSet, failedClientSet); + + return new ClusterAppAssignResultVO().setFailedClientSet(failedClientSet).setFailedServerSet(failedServerSet); + } + + private void applyAllRemainingMachineSet(String app, Set remainingSet, Set failedSet) { + if (remainingSet == null || remainingSet.isEmpty()) { + return; + } + remainingSet.parallelStream().filter(Objects::nonNull).map(MachineUtils::parseCommandIpAndPort) + .filter(Optional::isPresent).map(Optional::get).map(ipPort -> { + String ip = ipPort.r1; + int commandPort = ipPort.r2; + CompletableFuture f = modifyMode(ip, commandPort, ClusterStateManager.CLUSTER_NOT_STARTED); + return Tuple2.of(ip + '@' + commandPort, f); + }).forEach(t -> handleFutureSync(t, failedSet)); + } + + private void applyAllClientConfigChange(String app, ClusterAppAssignMap assignMap, Set failedSet) { + Set clientSet = assignMap.getClientSet(); + if (clientSet == null || clientSet.isEmpty()) { + return; + } + final String serverIp = assignMap.getIp(); + final int serverPort = assignMap.getPort(); + clientSet.stream().map(MachineUtils::parseCommandIpAndPort).filter(Optional::isPresent).map(Optional::get) + .map(ipPort -> { + CompletableFuture f = sentinelApiClient + .modifyClusterMode(ipPort.r1, ipPort.r2, ClusterStateManager.CLUSTER_CLIENT) + .thenCompose(v -> sentinelApiClient.modifyClusterClientConfig(app, ipPort.r1, ipPort.r2, + new ClusterClientConfig().setRequestTimeout(20).setServerHost(serverIp) + .setServerPort(serverPort))); + return Tuple2.of(ipPort.r1 + '@' + ipPort.r2, f); + }).forEach(t -> handleFutureSync(t, failedSet)); + } + + private void handleFutureSync(Tuple2> t, Set failedSet) { + try { + t.r2.get(10, TimeUnit.SECONDS); + } + catch (Exception ex) { + if (ex instanceof ExecutionException) { + LOGGER.error("Request for <{}> failed", t.r1, ex.getCause()); + } + else { + LOGGER.error("Request for <{}> failed", t.r1, ex); + } + failedSet.add(t.r1); + } + } + + private CompletableFuture applyServerConfigChange(String app, String ip, int commandPort, + ClusterAppAssignMap assignMap) { + ServerTransportConfig transportConfig = new ServerTransportConfig().setPort(assignMap.getPort()) + .setIdleSeconds(600); + return sentinelApiClient.modifyClusterServerTransportConfig(app, ip, commandPort, transportConfig) + .thenCompose(v -> applyServerFlowConfigChange(app, ip, commandPort, assignMap)) + .thenCompose(v -> applyServerNamespaceSetConfig(app, ip, commandPort, assignMap)); + } + + private CompletableFuture applyServerFlowConfigChange(String app, String ip, int commandPort, + ClusterAppAssignMap assignMap) { + Double maxAllowedQps = assignMap.getMaxAllowedQps(); + if (maxAllowedQps == null || maxAllowedQps <= 0 || maxAllowedQps > 20_0000) { + return CompletableFuture.completedFuture(null); + } + return sentinelApiClient.modifyClusterServerFlowConfig(app, ip, commandPort, + new ServerFlowConfig().setMaxAllowedQps(maxAllowedQps)); + } + + private CompletableFuture applyServerNamespaceSetConfig(String app, String ip, int commandPort, + ClusterAppAssignMap assignMap) { + Set namespaceSet = assignMap.getNamespaceSet(); + if (namespaceSet == null || namespaceSet.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + return sentinelApiClient.modifyClusterServerNamespaceSet(app, ip, commandPort, namespaceSet); + } + + private CompletableFuture modifyMode(String ip, int port, int mode) { + return sentinelApiClient.modifyClusterMode(ip, port, mode); + } + + private int parsePort(ClusterAppAssignMap assignMap) { + return MachineUtils.parseCommandPort(assignMap.getMachineId()).orElse(ServerTransportConfig.DEFAULT_PORT); + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterConfigService.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterConfigService.java new file mode 100644 index 0000000..d1cf10f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/service/ClusterConfigService.java @@ -0,0 +1,172 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.service; + +import com.alibaba.csp.sentinel.cluster.ClusterStateManager; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.discovery.AppInfo; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterGroupEntity; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ClusterClientConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerFlowConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerTransportConfig; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterClientModifyRequest; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterServerModifyRequest; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterClientStateVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterUniversalStatePairVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterUniversalStateVO; +import com.alibaba.csp.sentinel.dashboard.util.AsyncUtils; +import com.alibaba.csp.sentinel.dashboard.util.ClusterEntityUtils; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * @author Eric Zhao + * @since 1.4.0 + */ +@Service +public class ClusterConfigService { + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private AppManagement appManagement; + + public CompletableFuture modifyClusterClientConfig(ClusterClientModifyRequest request) { + if (notClientRequestValid(request)) { + throw new IllegalArgumentException("Invalid request"); + } + String app = request.getApp(); + String ip = request.getIp(); + int port = request.getPort(); + return sentinelApiClient.modifyClusterClientConfig(app, ip, port, request.getClientConfig()) + .thenCompose(v -> sentinelApiClient.modifyClusterMode(ip, port, ClusterStateManager.CLUSTER_CLIENT)); + } + + private boolean notClientRequestValid(/* @NonNull */ ClusterClientModifyRequest request) { + ClusterClientConfig config = request.getClientConfig(); + return config == null || StringUtil.isEmpty(config.getServerHost()) || config.getServerPort() == null + || config.getServerPort() <= 0 || config.getRequestTimeout() == null || config.getRequestTimeout() <= 0; + } + + public CompletableFuture modifyClusterServerConfig(ClusterServerModifyRequest request) { + ServerTransportConfig transportConfig = request.getTransportConfig(); + ServerFlowConfig flowConfig = request.getFlowConfig(); + Set namespaceSet = request.getNamespaceSet(); + if (invalidTransportConfig(transportConfig)) { + throw new IllegalArgumentException("Invalid transport config in request"); + } + if (invalidFlowConfig(flowConfig)) { + throw new IllegalArgumentException("Invalid flow config in request"); + } + if (namespaceSet == null) { + throw new IllegalArgumentException("namespace set cannot be null"); + } + String app = request.getApp(); + String ip = request.getIp(); + int port = request.getPort(); + return sentinelApiClient.modifyClusterServerNamespaceSet(app, ip, port, namespaceSet) + .thenCompose(v -> sentinelApiClient.modifyClusterServerTransportConfig(app, ip, port, transportConfig)) + .thenCompose(v -> sentinelApiClient.modifyClusterServerFlowConfig(app, ip, port, flowConfig)) + .thenCompose(v -> sentinelApiClient.modifyClusterMode(ip, port, ClusterStateManager.CLUSTER_SERVER)); + } + + /** + * Get cluster state list of all available machines of provided application. + * @param app application name + * @return cluster state list of all available machines of the application + * @since 1.4.1 + */ + public CompletableFuture> getClusterUniversalState(String app) { + if (StringUtil.isBlank(app)) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("app cannot be empty")); + } + AppInfo appInfo = appManagement.getDetailApp(app); + if (appInfo == null || appInfo.getMachines() == null) { + return CompletableFuture.completedFuture(new ArrayList<>()); + } + + List> futures = appInfo.getMachines().stream() + .filter(e -> e.isHealthy()) + .map(machine -> getClusterUniversalState(app, machine.getIp(), machine.getPort()) + .thenApply(e -> new ClusterUniversalStatePairVO(machine.getIp(), machine.getPort(), e))) + .collect(Collectors.toList()); + + return AsyncUtils.sequenceSuccessFuture(futures); + } + + public CompletableFuture getClusterUniversalStateForAppMachine(String app, String machineId) { + if (StringUtil.isBlank(app)) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("app cannot be empty")); + } + AppInfo appInfo = appManagement.getDetailApp(app); + if (appInfo == null || appInfo.getMachines() == null) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("app does not have machines")); + } + + boolean machineOk = appInfo.getMachines().stream().filter(e -> e.isHealthy()) + .map(e -> e.getIp() + '@' + e.getPort()).anyMatch(e -> e.equals(machineId)); + if (!machineOk) { + return AsyncUtils.newFailedFuture(new IllegalStateException("machine does not exist or disconnected")); + } + + return getClusterUniversalState(app).thenApply(ClusterEntityUtils::wrapToClusterGroup) + .thenCompose(e -> e.stream().filter(e1 -> e1.getMachineId().equals(machineId)).findAny() + .map(CompletableFuture::completedFuture) + .orElse(AsyncUtils.newFailedFuture(new IllegalStateException("not a server: " + machineId)))); + } + + public CompletableFuture getClusterUniversalState(String app, String ip, int port) { + return sentinelApiClient.fetchClusterMode(ip, port) + .thenApply(e -> new ClusterUniversalStateVO().setStateInfo(e)).thenCompose(vo -> { + if (vo.getStateInfo().getClientAvailable()) { + return sentinelApiClient.fetchClusterClientInfoAndConfig(ip, port) + .thenApply(cc -> vo.setClient(new ClusterClientStateVO().setClientConfig(cc))); + } + else { + return CompletableFuture.completedFuture(vo); + } + }).thenCompose(vo -> { + if (vo.getStateInfo().getServerAvailable()) { + return sentinelApiClient.fetchClusterServerBasicInfo(ip, port).thenApply(vo::setServer); + } + else { + return CompletableFuture.completedFuture(vo); + } + }); + } + + private boolean invalidTransportConfig(ServerTransportConfig transportConfig) { + return transportConfig == null || transportConfig.getPort() == null || transportConfig.getPort() <= 0 + || transportConfig.getIdleSeconds() == null || transportConfig.getIdleSeconds() <= 0; + } + + private boolean invalidFlowConfig(ServerFlowConfig flowConfig) { + return flowConfig == null || flowConfig.getSampleCount() == null || flowConfig.getSampleCount() <= 0 + || flowConfig.getIntervalMs() == null || flowConfig.getIntervalMs() <= 0 + || flowConfig.getIntervalMs() % flowConfig.getSampleCount() != 0 + || flowConfig.getMaxAllowedQps() == null || flowConfig.getMaxAllowedQps() < 0; + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/AsyncUtils.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/AsyncUtils.java new file mode 100644 index 0000000..b7372b2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/AsyncUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public final class AsyncUtils { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncUtils.class); + + public static CompletableFuture newFailedFuture(Throwable ex) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(ex); + return future; + } + + public static CompletableFuture> sequenceFuture(List> futures) { + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply( + v -> futures.stream().map(AsyncUtils::getValue).filter(Objects::nonNull).collect(Collectors.toList())); + } + + public static CompletableFuture> sequenceSuccessFuture(List> futures) { + return CompletableFuture.supplyAsync(() -> futures.parallelStream().map(AsyncUtils::getValue) + .filter(Objects::nonNull).collect(Collectors.toList())); + } + + public static T getValue(CompletableFuture future) { + try { + return future.get(10, TimeUnit.SECONDS); + } + catch (Exception ex) { + LOG.error("getValue for async result failed", ex); + } + return null; + } + + public static boolean isSuccessFuture(CompletableFuture future) { + return future.isDone() && !future.isCompletedExceptionally() && !future.isCancelled(); + } + + private AsyncUtils() { + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/ClusterEntityUtils.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/ClusterEntityUtils.java new file mode 100644 index 0000000..c43e92d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/ClusterEntityUtils.java @@ -0,0 +1,142 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.util; + +import com.alibaba.csp.sentinel.cluster.ClusterStateManager; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ClusterGroupEntity; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.ConnectionGroupVO; +import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.*; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.*; + +/** + * @author Eric Zhao + * @since 1.4.1 + */ +public final class ClusterEntityUtils { + + public static List wrapToAppClusterServerState( + List list) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + Map map = new HashMap<>(); + Set tokenServerSet = new HashSet<>(); + // Handle token servers that belong to current app. + for (ClusterUniversalStatePairVO stateVO : list) { + int mode = stateVO.getState().getStateInfo().getMode(); + + if (mode == ClusterStateManager.CLUSTER_SERVER) { + String ip = stateVO.getIp(); + String serverId = ip + '@' + stateVO.getCommandPort(); + ClusterServerStateVO serverStateVO = stateVO.getState().getServer(); + map.computeIfAbsent(serverId, + v -> new AppClusterServerStateWrapVO().setId(serverId).setIp(ip) + .setPort(serverStateVO.getPort()).setState(serverStateVO).setBelongToApp(true) + .setConnectedCount(serverStateVO.getConnection().stream() + .mapToInt(ConnectionGroupVO::getConnectedCount).sum())); + tokenServerSet.add(ip + ":" + serverStateVO.getPort()); + } + } + // Handle token servers from other app. + for (ClusterUniversalStatePairVO stateVO : list) { + int mode = stateVO.getState().getStateInfo().getMode(); + + if (mode == ClusterStateManager.CLUSTER_CLIENT) { + ClusterClientStateVO clientState = stateVO.getState().getClient(); + if (clientState == null) { + continue; + } + String serverIp = clientState.getClientConfig().getServerHost(); + int serverPort = clientState.getClientConfig().getServerPort(); + if (tokenServerSet.contains(serverIp + ":" + serverPort)) { + continue; + } + // We are not able to get the commandPort of foreign token server + // directly. + String serverId = String.format("%s:%d", serverIp, serverPort); + map.computeIfAbsent(serverId, v -> new AppClusterServerStateWrapVO().setId(serverId).setIp(serverIp) + .setPort(serverPort).setBelongToApp(false)); + } + } + return new ArrayList<>(map.values()); + } + + public static List wrapToAppClusterClientState( + List list) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + Map map = new HashMap<>(); + for (ClusterUniversalStatePairVO stateVO : list) { + int mode = stateVO.getState().getStateInfo().getMode(); + + if (mode == ClusterStateManager.CLUSTER_CLIENT) { + String ip = stateVO.getIp(); + String clientId = ip + '@' + stateVO.getCommandPort(); + ClusterClientStateVO clientStateVO = stateVO.getState().getClient(); + map.computeIfAbsent(clientId, v -> new AppClusterClientStateWrapVO().setId(clientId).setIp(ip) + .setState(clientStateVO).setCommandPort(stateVO.getCommandPort())); + } + } + return new ArrayList<>(map.values()); + } + + public static List wrapToClusterGroup(List list) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + Map map = new HashMap<>(); + for (ClusterUniversalStatePairVO stateVO : list) { + int mode = stateVO.getState().getStateInfo().getMode(); + String ip = stateVO.getIp(); + if (mode == ClusterStateManager.CLUSTER_SERVER) { + String serverAddress = getIp(ip); + int port = stateVO.getState().getServer().getPort(); + map.computeIfAbsent(serverAddress, v -> new ClusterGroupEntity().setBelongToApp(true) + .setMachineId(ip + '@' + stateVO.getCommandPort()).setIp(ip).setPort(port)); + } + } + for (ClusterUniversalStatePairVO stateVO : list) { + int mode = stateVO.getState().getStateInfo().getMode(); + String ip = stateVO.getIp(); + if (mode == ClusterStateManager.CLUSTER_CLIENT) { + String targetServer = stateVO.getState().getClient().getClientConfig().getServerHost(); + Integer targetPort = stateVO.getState().getClient().getClientConfig().getServerPort(); + if (StringUtil.isBlank(targetServer) || targetPort == null || targetPort <= 0) { + continue; + } + + ClusterGroupEntity group = map.computeIfAbsent(targetServer, v -> new ClusterGroupEntity() + .setBelongToApp(true).setMachineId(targetServer).setIp(targetServer).setPort(targetPort)); + group.getClientSet().add(ip + '@' + stateVO.getCommandPort()); + } + } + return new ArrayList<>(map.values()); + } + + private static String getIp(String str) { + if (str.contains(":")) { + return str.split(":")[0]; + } + return str; + } + + private ClusterEntityUtils() { + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/MachineUtils.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/MachineUtils.java new file mode 100644 index 0000000..c124aee --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/MachineUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.util; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.util.function.Tuple2; + +import java.util.Optional; + +/** + * @author Eric Zhao + */ +public final class MachineUtils { + + public static Optional parseCommandPort(String machineIp) { + try { + if (!machineIp.contains("@")) { + return Optional.empty(); + } + String[] str = machineIp.split("@"); + if (str.length <= 1) { + return Optional.empty(); + } + return Optional.of(Integer.parseInt(str[1])); + } + catch (Exception ex) { + return Optional.empty(); + } + } + + public static Optional> parseCommandIpAndPort(String machineIp) { + try { + if (StringUtil.isEmpty(machineIp) || !machineIp.contains("@")) { + return Optional.empty(); + } + String[] str = machineIp.split("@"); + if (str.length <= 1) { + return Optional.empty(); + } + return Optional.of(Tuple2.of(str[0], Integer.parseInt(str[1]))); + } + catch (Exception ex) { + return Optional.empty(); + } + } + + private MachineUtils() { + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/VersionUtils.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/VersionUtils.java new file mode 100644 index 0000000..cbc6f9e --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/VersionUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.util; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.Optional; + +/** + * Util class for parsing version. + * + * @author Eric Zhao + * @since 0.2.1 + */ +public final class VersionUtils { + + /** + * Parse version of Sentinel from raw string. + * @param versionFull version string + * @return parsed {@link SentinelVersion} if the version is valid; empty if there is + * something wrong with the format + */ + public static Optional parseVersion(String s) { + if (StringUtil.isBlank(s)) { + return Optional.empty(); + } + try { + String versionFull = s; + SentinelVersion version = new SentinelVersion(); + + // postfix + int index = versionFull.indexOf("-"); + if (index == 0) { + // Start with "-" + return Optional.empty(); + } + if (index == versionFull.length() - 1) { + // End with "-" + } + else if (index > 0) { + version.setPostfix(versionFull.substring(index + 1)); + } + + if (index >= 0) { + versionFull = versionFull.substring(0, index); + } + + // x.x.x + int segment = 0; + int[] ver = new int[3]; + while (segment < ver.length) { + index = versionFull.indexOf('.'); + if (index < 0) { + if (versionFull.length() > 0) { + ver[segment] = Integer.valueOf(versionFull); + } + break; + } + ver[segment] = Integer.valueOf(versionFull.substring(0, index)); + versionFull = versionFull.substring(index + 1); + segment++; + } + + if (ver[0] < 1) { + // Wrong format, return empty. + return Optional.empty(); + } + else { + return Optional.of(version.setMajorVersion(ver[0]).setMinorVersion(ver[1]).setFixVersion(ver[2])); + } + } + catch (Exception ex) { + // Parse fail, return empty. + return Optional.empty(); + } + } + + private VersionUtils() { + } + +} diff --git a/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/pool/PoolUtils.java b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/pool/PoolUtils.java new file mode 100644 index 0000000..ea0410c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/util/pool/PoolUtils.java @@ -0,0 +1,67 @@ +package com.alibaba.csp.sentinel.dashboard.util.pool; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author by zhaowenyuan create 2022/7/26 09:28 + * 线程池 + */ +public enum PoolUtils { + /** + * 线程池 + */ + SENTINEL("SENTINEL-POOL-SERVICE"), + ; + private final ThreadPoolExecutor instance; + + PoolUtils(String threadName){ + this.instance = new ThreadPoolExecutor( + 4, + 5, + 10L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(65535), + new NameThreadFactory(threadName)); + } + + public ThreadPoolExecutor getInstance(){ + return instance; + } + + public void destroy(){ + this.instance.shutdown(); + } + + static class NameThreadFactory implements ThreadFactory{ + private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); + private final ThreadGroup group; + + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + NameThreadFactory(String name){ + SecurityManager securityManager = System.getSecurityManager(); + group = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); + if (null == name || name.isEmpty()){ + name = "pool"; + } + namePrefix = name + "-" + POOL_NUMBER.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread t = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0); + if (t.isDaemon()){ + t.setDaemon(false); + } + if (t.getPriority() != Thread.NORM_PRIORITY){ + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + } +} diff --git a/chushang-visual/chushang-sentinel/src/main/resources/bootstrap.yml b/chushang-visual/chushang-sentinel/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..8592d45 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/resources/bootstrap.yml @@ -0,0 +1,30 @@ +server: + port: 5003 + servlet: + encoding: + force: true +spring: + application: + name: @artifactId@ + cloud: + nacos: + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + group: ${nacos.group} + config: + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + server-addr: ${spring.cloud.nacos.discovery.server-addr} + file-extension: yml + shared-configs: + - dataId: application-admin-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + group: COMMON_GROUP + - dataId: application-log-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + group: COMMON_GROUP + refresh: true + profiles: + active: @profiles.active@ +sentinel: + dashboard: + version: @project.version@ diff --git a/chushang-visual/chushang-sentinel/src/main/resources/logback-nacos.xml b/chushang-visual/chushang-sentinel/src/main/resources/logback-nacos.xml new file mode 100644 index 0000000..359e467 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/resources/logback-nacos.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + UTF-8 + + + + + ${log.path}/info.log + + ${log.path}/info.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + info + + + + + ${log.path}/debug.log + + ${log.path}/debug.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + debug + + + + + ${log.path}/error.log + + ${log.path}/error.%d{yyyy-MM-dd}-%i.log + 5 + 500MB + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/.gitignore b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/.gitignore new file mode 100644 index 0000000..10b731c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/.gitignore @@ -0,0 +1,5 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/inspectionProfiles/Project_Default.xml b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..eff7139 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/jsLinters/jshint.xml b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/jsLinters/jshint.xml new file mode 100644 index 0000000..8e9049d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/jsLinters/jshint.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/modules.xml b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/modules.xml new file mode 100644 index 0000000..e878207 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/vcs.xml b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/vcs.xml new file mode 100644 index 0000000..821e530 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/.jshintrc b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.jshintrc new file mode 100644 index 0000000..6c66001 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/.jshintrc @@ -0,0 +1,67 @@ +{ + /* + * ENVIRONMENTS + * ================= + */ + + // Define globals exposed by modern browsers. + "browser": true, + + // Define globals exposed by jQuery. + "jquery": true, + + // Define globals exposed by Node.js. + "node": true, + + // Allow ES6. + "esversion": 6, + + /* + * ENFORCING OPTIONS + * ================= + */ + + // Force all variable names to use either camelCase style or UPPER_CASE + // with underscores. + "camelcase": true, + + // Prohibit use of == and != in favor of === and !==. + "eqeqeq": true, + + // Enforce tab width of 2 spaces. + "indent": 2, + + // Prohibit use of a variable before it is defined. + "latedef": true, + + // Enforce line length to 100 characters + "maxlen": 100, + + // Require capitalized names for constructor functions. + "newcap": true, + + // Enforce use of single quotation marks for strings. + "quotmark": "single", + + // Enforce placing 'use strict' at the top function scope + // 前端项目中外层使用 strict 即可,覆盖此条规则 + "strict": false, + + // Prohibit use of explicitly undeclared variables. + "undef": true, + + // Warn when variables are defined but never used. + "unused": true, + + /* + * RELAXING OPTIONS + * ================= + */ + + // Suppress warnings about == null comparisons. + "eqnull": true, + "globals": { + "$": false, + "angular": false + } +} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/README.md b/chushang-visual/chushang-sentinel/src/main/webapp/resources/README.md new file mode 100644 index 0000000..c88ea68 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/README.md @@ -0,0 +1,32 @@ +# Sentinel Dashboard Frontend + +## Env Requirement + +- Node.js > 6.x + +## Code Guide + +- [Code Style Guide for HTML/CSS](https://codeguide.bootcss.com/) +- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript/tree/es5-deprecated/es5) + +## Install Packages + +``` +npm install +``` + +## Start Development + +``` +npm start +``` + +## Build for production + +``` +npm run build +``` + +## Credit + +- [sb-admin-angular](https://github.com/start-angular/sb-admin-angular) \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/README_zh.md b/chushang-visual/chushang-sentinel/src/main/webapp/resources/README_zh.md new file mode 100644 index 0000000..5daf33e --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/README_zh.md @@ -0,0 +1,32 @@ +# Sentinel Dashboard Frontend + +## 环境要求 + +- Node.js > 6.x + +## 编码规范 + +- HTML/CSS 遵循 [Bootstrap 编码规范](https://codeguide.bootcss.com/) +- JavaScript 遵循 [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript/tree/es5-deprecated/es5) 以及最新的 ES 6 标准 + +## 安装依赖 + +``` +npm i +``` + +## 开始本地开发 + +``` +npm start +``` + +## 构建前端资源 + +``` +npm run build +``` + +## Credit + +- [sb-admin-angular](https://github.com/start-angular/sb-admin-angular) \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/app.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/app.js new file mode 100644 index 0000000..bc3747a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/app.js @@ -0,0 +1,365 @@ +'use strict'; + +/** + * @ngdoc overview + * @name sentinelDashboardApp + * @description + * # sentinelDashboardApp + * + * Main module of the application. + */ + +angular + .module('sentinelDashboardApp', [ + 'oc.lazyLoad', + 'ui.router', + 'ui.bootstrap', + 'angular-loading-bar', + 'ngDialog', + 'ui.bootstrap.datetimepicker', + 'ui-notification', + 'rzTable', + 'angular-clipboard', + 'selectize', + 'angularUtils.directives.dirPagination' + ]) + .factory('AuthInterceptor', ['$window', '$state', function ($window, $state) { + var authInterceptor = { + 'responseError' : function(response) { + if (response.status === 401) { + // If not auth, clear session in localStorage and jump to the login page + $window.localStorage.removeItem('session_sentinel_admin'); + $state.go('login'); + } + + return response; + }, + 'response' : function(response) { + return response; + }, + 'request' : function(config) { + return config; + }, + 'requestError' : function(config){ + return config; + } + }; + return authInterceptor; + }]) + .config(['$stateProvider', '$urlRouterProvider', '$ocLazyLoadProvider', '$httpProvider', + function ($stateProvider, $urlRouterProvider, $ocLazyLoadProvider, $httpProvider) { + $httpProvider.interceptors.push('AuthInterceptor'); + + $ocLazyLoadProvider.config({ + debug: false, + events: true, + }); + + $urlRouterProvider.otherwise('/dashboard/home'); + + $stateProvider + .state('login', { + url: '/login', + templateUrl: 'app/views/login.html', + controller: 'LoginCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/login.js', + ] + }); + }] + } + }) + + .state('dashboard', { + url: '/dashboard', + templateUrl: 'app/views/dashboard/main.html', + resolve: { + loadMyDirectives: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load( + { + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/directives/header/header.js', + 'app/scripts/directives/sidebar/sidebar.js', + 'app/scripts/directives/sidebar/sidebar-search/sidebar-search.js', + ] + }); + }] + } + }) + + .state('dashboard.home', { + url: '/home', + templateUrl: 'app/views/dashboard/home.html', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/main.js', + ] + }); + }] + } + }) + + .state('dashboard.flowV1', { + templateUrl: 'app/views/flow_v1.html', + url: '/flow/:app', + controller: 'FlowControllerV1', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/flow_v1.js', + ] + }); + }] + } + }) + + .state('dashboard.flow', { + templateUrl: 'app/views/flow_v2.html', + url: '/v2/flow/:app', + controller: 'FlowControllerV2', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/flow_v2.js', + ] + }); + }] + } + }) + + .state('dashboard.paramFlow', { + templateUrl: 'app/views/param_flow.html', + url: '/paramFlow/:app', + controller: 'ParamFlowController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/param_flow.js', + ] + }); + }] + } + }) + + .state('dashboard.clusterAppAssignManage', { + templateUrl: 'app/views/cluster_app_assign_manage.html', + url: '/cluster/assign_manage/:app', + controller: 'SentinelClusterAppAssignManageController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_assign_manage.js', + ] + }); + }] + } + }) + + .state('dashboard.clusterAppServerList', { + templateUrl: 'app/views/cluster_app_server_list.html', + url: '/cluster/server/:app', + controller: 'SentinelClusterAppServerListController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_server_list.js', + ] + }); + }] + } + }) + + .state('dashboard.clusterAppClientList', { + templateUrl: 'app/views/cluster_app_client_list.html', + url: '/cluster/client/:app', + controller: 'SentinelClusterAppTokenClientListController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_token_client_list.js', + ] + }); + }] + } + }) + + .state('dashboard.clusterSingle', { + templateUrl: 'app/views/cluster_single_config.html', + url: '/cluster/single/:app', + controller: 'SentinelClusterSingleController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_single.js', + ] + }); + }] + } + }) + + .state('dashboard.authority', { + templateUrl: 'app/views/authority.html', + url: '/authority/:app', + controller: 'AuthorityRuleController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/authority.js', + ] + }); + }] + } + }) + + .state('dashboard.degrade', { + templateUrl: 'app/views/degrade.html', + url: '/degrade/:app', + controller: 'DegradeCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/degrade.js', + ] + }); + }] + } + }) + + .state('dashboard.system', { + templateUrl: 'app/views/system.html', + url: '/system/:app', + controller: 'SystemCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/system.js', + ] + }); + }] + } + }) + + .state('dashboard.machine', { + templateUrl: 'app/views/machine.html', + url: '/app/:app', + controller: 'MachineCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/machine.js', + ] + }); + }] + } + }) + + .state('dashboard.identity', { + templateUrl: 'app/views/identity.html', + url: '/identity/:app', + controller: 'IdentityCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/identity.js', + ] + }); + }] + } + }) + + .state('dashboard.gatewayIdentity', { + templateUrl: 'app/views/gateway/identity.html', + url: '/gateway/identity/:app', + controller: 'GatewayIdentityCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/identity.js', + ] + }); + }] + } + }) + + .state('dashboard.metric', { + templateUrl: 'app/views/metric.html', + url: '/metric/:app', + controller: 'MetricCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/metric.js', + ] + }); + }] + } + }) + + .state('dashboard.gatewayApi', { + templateUrl: 'app/views/gateway/api.html', + url: '/gateway/api/:app', + controller: 'GatewayApiCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/api.js', + ] + }); + }] + } + }) + + .state('dashboard.gatewayFlow', { + templateUrl: 'app/views/gateway/flow.html', + url: '/gateway/flow/:app', + controller: 'GatewayFlowCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/flow.js', + ] + }); + }] + } + }); + }]); \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/authority.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/authority.js new file mode 100644 index 0000000..3d86302 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/authority.js @@ -0,0 +1,227 @@ +/** + * Authority rule controller. + */ +angular.module('sentinelDashboardApp').controller('AuthorityRuleController', ['$scope', '$stateParams', 'AuthorityRuleService', 'ngDialog', + 'MachineService', + function ($scope, $stateParams, AuthorityRuleService, ngDialog, + MachineService) { + $scope.app = $stateParams.app; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + let mac = $scope.macInputModel.split(':'); + AuthorityRuleService.queryMachineRules($scope.app, mac[0], mac[1]) + .success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + $scope.loadError = {message: data.msg}; + } + }) + .error((data, header, config, status) => { + $scope.loadError = {message: "未知错误"}; + }); + }; + $scope.getMachineRules = getMachineRules; + getMachineRules(); + + var authorityRuleDialog; + + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.authorityRuleDialog = { + title: '编辑授权规则', + type: 'edit', + confirmBtnText: '保存', + }; + authorityRuleDialog = ngDialog.open({ + template: '/app/views/dialog/authority-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + app: $scope.app, + ip: mac[0], + port: mac[1], + rule: { + strategy: 0, + limitApp: '', + } + }; + $scope.authorityRuleDialog = { + title: '新增授权规则', + type: 'add', + confirmBtnText: '新增', + showAdvanceButton: true, + }; + authorityRuleDialog = ngDialog.open({ + template: '/app/views/dialog/authority-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if (!AuthorityRuleService.checkRuleValid($scope.currentRule.rule)) { + return; + } + if ($scope.authorityRuleDialog.type === 'add') { + addNewRuleAndPush($scope.currentRule); + } else if ($scope.authorityRuleDialog.type === 'edit') { + saveRuleAndPush($scope.currentRule, true); + } + }; + + function addNewRuleAndPush(rule) { + AuthorityRuleService.addNewRule(rule).success((data) => { + if (data.success) { + getMachineRules(); + authorityRuleDialog.close(); + } else { + alert('添加规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('添加规则失败:' + data.msg); + } else { + alert("添加规则失败:未知错误"); + } + }); + } + + function saveRuleAndPush(rule, edit) { + AuthorityRuleService.saveRule(rule).success(function (data) { + if (data.success) { + alert("修改规则成功"); + getMachineRules(); + if (edit) { + authorityRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('修改规则失败:' + data.msg); + } else { + alert("修改规则失败:未知错误"); + } + }); + } + + function deleteRuleAndPush(entity) { + if (entity.id === undefined || isNaN(entity.id)) { + alert('规则 ID 不合法!'); + return; + } + AuthorityRuleService.deleteRule(entity).success((data) => { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('删除规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('删除规则失败:' + data.msg); + } else { + alert("删除规则失败:未知错误"); + } + }); + }; + + var confirmDialog; + $scope.deleteRule = function (ruleEntity) { + $scope.currentRule = ruleEntity; + $scope.confirmDialog = { + title: '删除授权规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下授权限流规则', + attention: '资源名: ' + ruleEntity.rule.resource + ', 流控应用: ' + ruleEntity.rule.limitApp + + ', 类型: ' + (ruleEntity.rule.strategy === 0 ? '白名单' : '黑名单'), + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type === 'delete_rule') { + deleteRuleAndPush($scope.currentRule); + } else { + console.error('error'); + } + }; + + queryAppMachines(); + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_assign_manage.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_assign_manage.js new file mode 100644 index 0000000..6f9367d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_assign_manage.js @@ -0,0 +1,283 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SentinelClusterAppAssignManageController', ['$scope', '$stateParams', 'ngDialog', + 'MachineService', 'ClusterStateService', + function ($scope, $stateParams, ngDialog, MachineService, ClusterStateService) { + $scope.app = $stateParams.app; + const UNSUPPORTED_CODE = 4041; + + const CLUSTER_MODE_CLIENT = 0; + const CLUSTER_MODE_SERVER = 1; + const DEFAULT_CLUSTER_SERVER_PORT = 18730; + + $scope.tmp = { + curClientChosen: [], + curRemainingClientChosen: [], + curChosenServer: {}, + }; + + function convertSetToString(set) { + if (set === undefined) { + return ''; + } + let s = ''; + for (let i = 0; i < set.length; i++) { + s = s + set[i]; + if (i < set.length - 1) { + s = s + ','; + } + } + return s; + } + + function convertStrToNamespaceSet(str) { + if (str === undefined || str === '') { + return []; + } + let arr = []; + let spliced = str.split(','); + spliced.forEach((v) => { + arr.push(v.trim()); + }); + return arr; + } + + function processAppSingleData(data) { + if (data.state.server && data.state.server.namespaceSet) { + data.state.server.namespaceSetStr = convertSetToString(data.state.server.namespaceSet); + data.mode = data.state.stateInfo.mode; + } + } + + function removeFromArr(arr, v) { + for (let i = 0; i < arr.length; i++) { + if (arr[i] === v) { + arr.splice(i, 1); + break; + } + } + } + + function resetChosen() { + $scope.tmp.curClientChosen = []; + $scope.tmp.curRemainingClientChosen = []; + } + + function generateMachineId(e) { + return e.ip + '@' + e.commandPort; + } + + function applyClusterMap(appClusterMachineList) { + if (!appClusterMachineList) { + return; + } + let tmpMap = new Map(); + $scope.clusterMap = []; + $scope.remainingClientAddressList = []; + let tmpServerList = []; + let tmpClientList = []; + appClusterMachineList.forEach((e) => { + if (e.mode === CLUSTER_MODE_CLIENT) { + tmpClientList.push(e); + } else if (e.mode === CLUSTER_MODE_SERVER) { + tmpServerList.push(e); + } else { + $scope.remainingClientAddressList.push(generateMachineId(e)); + } + }); + tmpServerList.forEach((e) => { + let ip = e.ip; + let machineId = ip + '@' + e.commandPort; + let group = { + ip: ip, + machineId: machineId, + port: e.state.server.port, + clientSet: [], + namespaceSetStr: e.state.server.namespaceSetStr, + belongToApp: true, + }; + if (!tmpMap.has(ip)) { + tmpMap.set(ip, group); + } + }); + tmpClientList.forEach((e) => { + let ip = e.ip; + let machineId = ip + '@' + e.commandPort; + + let targetServer = e.state.client.clientConfig.serverHost; + let targetPort = e.state.client.clientConfig.serverPort; + if (targetServer === undefined || targetServer === '' || + targetPort === undefined || targetPort <= 0) { + $scope.remainingClientAddressList.push(generateMachineId(e)); + return; + } + + if (!tmpMap.has(targetServer)) { + let group = { + ip: targetServer, + machineId: targetServer, + port: targetPort, + clientSet: [machineId], + belongToApp: false, + }; + tmpMap.set(targetServer, group); + } else { + let g = tmpMap.get(targetServer); + g.clientSet.push(machineId); + } + }); + tmpMap.forEach((v) => { + if (v !== undefined) { + $scope.clusterMap.push(v); + } + }); + } + + $scope.onCurrentServerChange = () => { + resetChosen(); + }; + + $scope.remainingClientAddressList = []; + + $scope.moveToServerGroup = () => { + let chosenServer = $scope.tmp.curChosenServer; + if (!chosenServer || !chosenServer.machineId) { + return; + } + $scope.tmp.curRemainingClientChosen.forEach(e => { + chosenServer.clientSet.push(e); + removeFromArr($scope.remainingClientAddressList, e); + }); + resetChosen(); + }; + + $scope.moveToRemainingSharePool = () => { + $scope.tmp.curClientChosen.forEach(e => { + $scope.remainingClientAddressList.push(e); + removeFromArr($scope.tmp.curChosenServer.clientSet, e); + }); + resetChosen(); + }; + + function parseIpFromMachineId(machineId) { + if (machineId.indexOf('@') === -1) { + return machineId; + } + let arr = machineId.split('@'); + return arr[0]; + } + + $scope.addToServerList = () => { + let group; + $scope.tmp.curRemainingClientChosen.forEach(e => { + group = { + machineId: e, + ip: parseIpFromMachineId(e), + port: DEFAULT_CLUSTER_SERVER_PORT, + clientSet: [], + namespaceSetStr: 'default,' + $scope.app, + belongToApp: true, + }; + $scope.clusterMap.push(group); + removeFromArr($scope.remainingClientAddressList, e); + $scope.tmp.curChosenServer = group; + }); + resetChosen(); + }; + + $scope.removeFromServerList = () => { + let chosenServer = $scope.tmp.curChosenServer; + if (!chosenServer || !chosenServer.machineId) { + return; + } + chosenServer.clientSet.forEach((e) => { + if (e !== undefined) { + $scope.remainingClientAddressList.push(e); + } + }); + + if (chosenServer.belongToApp || chosenServer.machineId.indexOf('@') !== -1) { + $scope.remainingClientAddressList.push(chosenServer.machineId); + } else { + alert('提示:非本应用内机器将不会置回空闲列表中'); + } + + removeFromArr($scope.clusterMap, chosenServer); + + resetChosen(); + + if ($scope.clusterMap.length > 0) { + $scope.tmp.curChosenServer = $scope.clusterMap[0]; + $scope.onCurrentServerChange(); + } else { + $scope.tmp.curChosenServer = {}; + } + }; + + function retrieveClusterAppInfo() { + ClusterStateService.fetchClusterUniversalStateOfApp($scope.app).success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.appClusterMachineList = data.data; + $scope.appClusterMachineList.forEach(processAppSingleData); + applyClusterMap($scope.appClusterMachineList); + if ($scope.clusterMap.length > 0) { + $scope.tmp.curChosenServer = $scope.clusterMap[0]; + $scope.onCurrentServerChange(); + } + } else { + $scope.appClusterMachineList = {}; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }).error(() => { + $scope.loadError = {message: '未知错误'}; + }); + } + + retrieveClusterAppInfo(); + + $scope.saveAndApplyAssign = () => { + let ok = confirm('是否确认执行变更?'); + if (!ok) { + return; + } + let cm = $scope.clusterMap; + if (!cm) { + cm = []; + } + cm.forEach((e) => { + e.namespaceSet = convertStrToNamespaceSet(e.namespaceSetStr); + }); + cm.namespaceSet = convertStrToNamespaceSet(cm.namespaceSetStr); + let request = { + clusterMap: cm, + remainingList: $scope.remainingClientAddressList, + }; + ClusterStateService.applyClusterFullAssignOfApp($scope.app, request).success((data) => { + if (data.code === 0 && data.data) { + let failedServerSet = data.data.failedServerSet; + let failedClientSet = data.data.failedClientSet; + if (failedClientSet.length === 0 && failedServerSet.length === 0) { + alert('全部推送成功'); + } else { + alert('推送完毕。token server 失败列表:' + JSON.stringify(failedServerSet) + + '; token client 失败列表:' + JSON.stringify(failedClientSet)); + } + + retrieveClusterAppInfo(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('推送失败:' + data.msg); + } + } + }).error(() => { + alert('未知错误'); + }); + }; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_list.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_list.js new file mode 100644 index 0000000..7e1708c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_list.js @@ -0,0 +1,570 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SentinelClusterAppServerListController', ['$scope', '$stateParams', 'ngDialog', + 'MachineService', 'ClusterStateService', + function ($scope, $stateParams, ngDialog, MachineService, ClusterStateService) { + $scope.app = $stateParams.app; + const UNSUPPORTED_CODE = 4041; + + const CLUSTER_MODE_CLIENT = 0; + const CLUSTER_MODE_SERVER = 1; + const DEFAULT_CLUSTER_SERVER_PORT = 18730; + const DEFAULT_NAMESPACE = 'default'; + const DEFAULT_MAX_ALLOWED_QPS = 20000; + + // tmp for dialog temporary data. + $scope.tmp = { + curClientChosen: [], + curRemainingClientChosen: [], + curChosenServer: {}, + }; + + $scope.remainingMachineList = []; + + function convertSetToString(set) { + if (set === undefined) { + return ''; + } + if (set.length === 1 && set[0] === DEFAULT_NAMESPACE) { + return DEFAULT_NAMESPACE; + } + let s = ''; + for (let i = 0; i < set.length; i++) { + let ns = set[i]; + if (ns !== DEFAULT_NAMESPACE) { + s = s + ns; + if (i < set.length - 1) { + s = s + ','; + } + } + } + return s; + } + + function convertStrToNamespaceSet(str) { + if (str === undefined || str === '') { + return []; + } + let arr = []; + let spliced = str.split(','); + spliced.forEach((v) => { + arr.push(v.trim()); + }); + return arr; + } + + function processAppSingleData(data) { + if (data.state.server && data.state.server.namespaceSet) { + data.state.server.namespaceSetStr = convertSetToString(data.state.server.namespaceSet); + data.mode = data.state.stateInfo.mode; + } + } + + function removeFromArr(arr, v) { + for (let i = 0; i < arr.length; i++) { + if (arr[i] === v) { + arr.splice(i, 1); + break; + } + } + } + + function removeFromArrIf(arr, f) { + for (let i = 0; i < arr.length; i++) { + if (f(arr[i]) === true) { + arr.splice(i, 1); + break; + } + } + } + + function resetAssignDialogChosen() { + $scope.tmp.curClientChosen = []; + $scope.tmp.curRemainingClientChosen = []; + } + + function generateMachineId(e) { + return e.ip + '@' + e.commandPort; + } + + function applyClusterMap(appClusterMachineList) { + if (!appClusterMachineList) { + return; + } + let tmpMap = new Map(); + let serverCommandPortMap = new Map(); + $scope.clusterMap = []; + $scope.remainingMachineList = []; + let tmpServerList = []; + let tmpClientList = []; + appClusterMachineList.forEach((e) => { + if (e.mode === CLUSTER_MODE_CLIENT) { + tmpClientList.push(e); + } else if (e.mode === CLUSTER_MODE_SERVER) { + tmpServerList.push(e); + } else { + $scope.remainingMachineList.push(generateMachineId(e)); + } + }); + tmpServerList.forEach((e) => { + let ip = e.ip; + let machineId = ip + '@' + e.commandPort; + let group = { + ip: ip, + machineId: machineId, + port: e.state.server.port, + clientSet: [], + namespaceSetStr: e.state.server.namespaceSetStr, + maxAllowedQps: e.state.server.flow.maxAllowedQps, + belongToApp: true, + }; + if (!tmpMap.has(machineId)) { + tmpMap.set(machineId, group); + } + serverCommandPortMap.set(ip + ':' + e.state.server.port, e.commandPort); + }); + tmpClientList.forEach((e) => { + let ip = e.ip; + let machineId = ip + '@' + e.commandPort; + + let targetServer = e.state.client.clientConfig.serverHost; + let targetPort = e.state.client.clientConfig.serverPort; + if (targetServer === undefined || targetServer === '' || + targetPort === undefined || targetPort <= 0) { + $scope.remainingMachineList.push(generateMachineId(e)); + return; + } + + let serverHostPort = targetServer + ':' + targetPort; + + if (serverCommandPortMap.has(serverHostPort)) { + let serverCommandPort = serverCommandPortMap.get(serverHostPort); + let g; + if (serverCommandPort < 0) { + // Not belong to this app. + g = tmpMap.get(serverHostPort); + } else { + // Belong to this app. + g = tmpMap.get(targetServer + '@' + serverCommandPort); + } + g.clientSet.push(machineId); + } else { + let group = { + ip: targetServer, + machineId: serverHostPort, + port: targetPort, + clientSet: [machineId], + belongToApp: false, + }; + tmpMap.set(serverHostPort, group); + // Indicates that it's not belonging to current app. + serverCommandPortMap.set(serverHostPort, -1); + } + + // if (!tmpMap.has(serverHostPort)) { + // let group = { + // ip: targetServer, + // machineId: targetServer, + // port: targetPort, + // clientSet: [machineId], + // belongToApp: false, + // }; + // tmpMap.set(targetServer, group); + // } else { + // let g = tmpMap.get(targetServer); + // g.clientSet.push(machineId); + // } + }); + tmpMap.forEach((v) => { + if (v !== undefined) { + $scope.clusterMap.push(v); + } + }); + } + + $scope.notChosenServer = (id) => { + return id !== $scope.serverAssignDialogData.serverData.currentServer; + }; + + $scope.onCurrentServerChange = () => { + resetAssignDialogChosen(); + }; + + $scope.moveToServerGroup = () => { + $scope.tmp.curRemainingClientChosen.forEach(e => { + $scope.serverAssignDialogData.serverData.clientSet.push(e); + removeFromArr($scope.remainingMachineList, e); + }); + resetAssignDialogChosen(); + }; + + $scope.moveToRemainingSharePool = () => { + $scope.tmp.curClientChosen.forEach(e => { + $scope.remainingMachineList.push(e); + removeFromArr($scope.serverAssignDialogData.serverData.clientSet, e); + }); + resetAssignDialogChosen(); + }; + + function parseIpFromMachineId(machineId) { + if (machineId.indexOf(':') !== -1) { + return machineId.split(':')[0]; + } + if (machineId.indexOf('@') === -1) { + return machineId; + } + let arr = machineId.split('@'); + return arr[0]; + } + + function retrieveClusterAssignInfoOfApp() { + ClusterStateService.fetchClusterUniversalStateOfApp($scope.app).success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.appClusterMachineList = data.data; + $scope.appClusterMachineList.forEach(processAppSingleData); + applyClusterMap($scope.appClusterMachineList); + } else { + $scope.appClusterMachineList = {}; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }).error(() => { + $scope.loadError = {message: '未知错误'}; + }); + } + + + $scope.newServerDialog = () => { + retrieveClusterAssignInfoOfApp(); + $scope.serverAssignDialogData = { + title: '新增 Token Server', + type: 'add', + confirmBtnText: '保存', + serverData: { + serverType: 0, + clientSet: [], + serverPort: DEFAULT_CLUSTER_SERVER_PORT, + maxAllowedQps: DEFAULT_MAX_ALLOWED_QPS, + } + }; + $scope.serverAssignDialog = ngDialog.open({ + template: '/app/views/dialog/cluster/cluster-server-assign-dialog.html', + width: 1000, + overlay: true, + scope: $scope + }); + }; + + $scope.modifyServerAssignConfig = (serverVO) => { + let id = serverVO.id; + ClusterStateService.fetchClusterUniversalStateOfApp($scope.app).success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.appClusterMachineList = data.data; + $scope.appClusterMachineList.forEach(processAppSingleData); + applyClusterMap($scope.appClusterMachineList); + let clusterMap = $scope.clusterMap; + let d; + for (let i = 0; i < clusterMap.length; i++) { + if (clusterMap[i].machineId === id) { + d = clusterMap[i]; + } + } + if (!d) { + alert('状态错误'); + return; + } + $scope.serverAssignDialogData = { + title: 'Token Server 分配编辑', + type: 'edit', + confirmBtnText: '保存', + serverData: { + currentServer: d.machineId, + belongToApp: serverVO.belongToApp, + serverPort: d.port, + clientSet: d.clientSet, + } + }; + if (d.maxAllowedQps !== undefined) { + $scope.serverAssignDialogData.serverData.maxAllowedQps = d.maxAllowedQps; + } + $scope.serverAssignDialog = ngDialog.open({ + template: '/app/views/dialog/cluster/cluster-server-assign-dialog.html', + width: 1000, + overlay: true, + scope: $scope + }); + } else { + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }).error(() => { + $scope.loadError = {message: '未知错误'}; + }); + }; + + function getRemainingMachineList() { + return $scope.remainingMachineList.filter((e) => $scope.notChosenServer(e)); + } + + function doApplyNewSingleServerAssign() { + let ok = confirm('是否确认执行变更?'); + if (!ok) { + return; + } + let serverData = $scope.serverAssignDialogData.serverData; + let belongToApp = serverData.serverType == 0; // don't modify here! + let machineId = serverData.currentServer; + let request = { + clusterMap: { + machineId: machineId, + ip: parseIpFromMachineId(machineId), + port: serverData.serverPort, + clientSet: serverData.clientSet, + belongToApp: belongToApp, + maxAllowedQps: serverData.maxAllowedQps, + }, + remainingList: getRemainingMachineList(), + }; + ClusterStateService.applyClusterSingleServerAssignOfApp($scope.app, request).success((data) => { + if (data.code === 0 && data.data) { + let failedServerSet = data.data.failedServerSet; + let failedClientSet = data.data.failedClientSet; + if (failedClientSet.length === 0 && failedServerSet.length === 0) { + alert('全部推送成功'); + } else { + let failedSet = []; + if (failedServerSet) { + failedServerSet.forEach((e) => { + failedSet.push(e); + }); + } + if (failedClientSet) { + failedClientSet.forEach((e) => { + failedSet.push(e); + }); + } + + alert('推送完毕。失败机器列表:' + JSON.stringify(failedSet)); + } + + location.reload(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('推送失败:' + data.msg); + } + } + }).error(() => { + alert('未知错误'); + }); + } + + function doApplySingleServerAssignEdit() { + let ok = confirm('是否确认执行变更?'); + if (!ok) { + return; + } + let serverData = $scope.serverAssignDialogData.serverData; + let machineId = serverData.currentServer; + let request = { + clusterMap: { + machineId: machineId, + ip: parseIpFromMachineId(machineId), + port: serverData.serverPort, + clientSet: serverData.clientSet, + belongToApp: serverData.belongToApp, + }, + remainingList: $scope.remainingMachineList, + }; + if (serverData.maxAllowedQps !== undefined) { + request.clusterMap.maxAllowedQps = serverData.maxAllowedQps; + } + ClusterStateService.applyClusterSingleServerAssignOfApp($scope.app, request).success((data) => { + if (data.code === 0 && data.data) { + let failedServerSet = data.data.failedServerSet; + let failedClientSet = data.data.failedClientSet; + if (failedClientSet.length === 0 && failedServerSet.length === 0) { + alert('全部推送成功'); + } else { + let failedSet = []; + failedServerSet.forEach(failedSet.push); + failedClientSet.forEach(failedSet.push); + alert('推送完毕。失败机器列表:' + JSON.stringify(failedSet)); + } + + location.reload(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('推送失败:' + data.msg); + } + } + }).error(() => { + alert('未知错误'); + }); + } + + $scope.saveAssignForDialog = () => { + if (!checkAssignDialogValid()) { + return; + } + if ($scope.serverAssignDialogData.type === 'add') { + doApplyNewSingleServerAssign(); + } else if ($scope.serverAssignDialogData.type === 'edit') { + doApplySingleServerAssignEdit(); + } else { + alert('未知的操作'); + } + }; + + function checkAssignDialogValid() { + let serverData = $scope.serverAssignDialogData.serverData; + if (serverData.currentServer === undefined || serverData.currentServer === '') { + alert('请指定有效的 Token Server'); + return false; + } + if (serverData.serverPort === undefined || serverData.serverPort <= 0 || serverData.serverPort > 65535) { + alert('请输入合法的端口值'); + return false; + } + if (serverData.maxAllowedQps !== undefined && serverData.maxAllowedQps < 0) { + alert('请输入合法的最大允许 QPS'); + return false; + } + return true; + } + + $scope.viewConnectionDetail = (serverVO) => { + $scope.connectionDetailDialogData = { + serverData: serverVO + }; + $scope.connectionDetailDialog = ngDialog.open({ + template: '/app/views/dialog/cluster/cluster-server-connection-detail-dialog.html', + width: 700, + overlay: true, + scope: $scope + }); + }; + + function generateRequestLimitDataStr(limitData) { + if (limitData.length === 1 && limitData[0].namespace === DEFAULT_NAMESPACE) { + return 'default: ' + limitData[0].currentQps + ' / ' + limitData[0].maxAllowedQps; + } + for (let i = 0; i < limitData.length; i++) { + let crl = limitData[i]; + if (crl.namespace === $scope.app) { + return '' + crl.currentQps + ' / ' + crl.maxAllowedQps; + } + } + return '0'; + } + + function processServerListData(serverVO) { + if (serverVO.state && serverVO.state.namespaceSet) { + serverVO.state.namespaceSetStr = convertSetToString(serverVO.state.namespaceSet); + } + if (serverVO.state && serverVO.state.requestLimitData) { + serverVO.state.requestLimitDataStr = generateRequestLimitDataStr(serverVO.state.requestLimitData); + } + } + + $scope.generateConnectionSet = (data) => { + let connectionSet = data; + let s = ''; + if (connectionSet) { + s = s + '['; + for (let i = 0; i < connectionSet.length; i++) { + s = s + connectionSet[i].address; + if (i < connectionSet.length - 1) { + s = s + ', '; + } + } + s = s + ']'; + } else { + s = '[]'; + } + return s; + }; + + function retrieveClusterServerInfo() { + ClusterStateService.fetchClusterServerStateOfApp($scope.app).success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.serverVOList = data.data; + $scope.serverVOList.forEach(processServerListData); + } else { + $scope.serverVOList = {}; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }).error(() => { + $scope.loadError = {message: '未知错误'}; + }); + } + + retrieveClusterServerInfo(); + + let confirmUnbindServerDialog; + $scope.unbindServer = (id) => { + $scope.pendingUnbindIds = [id]; + $scope.confirmDialog = { + title: '移除 Token Server', + type: 'unbind_token_server', + attentionTitle: '请确认是否移除以下 Token Server(该 server 下的 client 也会解除分配)', + attention: id + '', + confirmBtnText: '移除', + }; + confirmUnbindServerDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + function apiUnbindServerAssign(ids) { + ClusterStateService.applyClusterServerBatchUnbind($scope.app, ids).success((data) => { + if (data.code === 0 && data.data) { + let failedServerSet = data.data.failedServerSet; + let failedClientSet = data.data.failedClientSet; + if (failedClientSet.length === 0 && failedServerSet.length === 0) { + alert('成功'); + } else { + alert('操作推送完毕,部分失败机器列表:' + JSON.stringify(failedClientSet)); + } + + location.reload(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('推送失败:' + data.msg); + } + } + }).error(() => { + alert('未知错误'); + }); + // confirmUnbindServerDialog.close(); + } + + // Confirm function for confirm dialog. + $scope.confirm = () => { + if ($scope.confirmDialog.type === 'unbind_token_server') { + apiUnbindServerAssign($scope.pendingUnbindIds); + } else { + console.error('Error dialog when unbinding token server'); + } + }; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_manage.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_manage.js new file mode 100644 index 0000000..6f9367d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_manage.js @@ -0,0 +1,283 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SentinelClusterAppAssignManageController', ['$scope', '$stateParams', 'ngDialog', + 'MachineService', 'ClusterStateService', + function ($scope, $stateParams, ngDialog, MachineService, ClusterStateService) { + $scope.app = $stateParams.app; + const UNSUPPORTED_CODE = 4041; + + const CLUSTER_MODE_CLIENT = 0; + const CLUSTER_MODE_SERVER = 1; + const DEFAULT_CLUSTER_SERVER_PORT = 18730; + + $scope.tmp = { + curClientChosen: [], + curRemainingClientChosen: [], + curChosenServer: {}, + }; + + function convertSetToString(set) { + if (set === undefined) { + return ''; + } + let s = ''; + for (let i = 0; i < set.length; i++) { + s = s + set[i]; + if (i < set.length - 1) { + s = s + ','; + } + } + return s; + } + + function convertStrToNamespaceSet(str) { + if (str === undefined || str === '') { + return []; + } + let arr = []; + let spliced = str.split(','); + spliced.forEach((v) => { + arr.push(v.trim()); + }); + return arr; + } + + function processAppSingleData(data) { + if (data.state.server && data.state.server.namespaceSet) { + data.state.server.namespaceSetStr = convertSetToString(data.state.server.namespaceSet); + data.mode = data.state.stateInfo.mode; + } + } + + function removeFromArr(arr, v) { + for (let i = 0; i < arr.length; i++) { + if (arr[i] === v) { + arr.splice(i, 1); + break; + } + } + } + + function resetChosen() { + $scope.tmp.curClientChosen = []; + $scope.tmp.curRemainingClientChosen = []; + } + + function generateMachineId(e) { + return e.ip + '@' + e.commandPort; + } + + function applyClusterMap(appClusterMachineList) { + if (!appClusterMachineList) { + return; + } + let tmpMap = new Map(); + $scope.clusterMap = []; + $scope.remainingClientAddressList = []; + let tmpServerList = []; + let tmpClientList = []; + appClusterMachineList.forEach((e) => { + if (e.mode === CLUSTER_MODE_CLIENT) { + tmpClientList.push(e); + } else if (e.mode === CLUSTER_MODE_SERVER) { + tmpServerList.push(e); + } else { + $scope.remainingClientAddressList.push(generateMachineId(e)); + } + }); + tmpServerList.forEach((e) => { + let ip = e.ip; + let machineId = ip + '@' + e.commandPort; + let group = { + ip: ip, + machineId: machineId, + port: e.state.server.port, + clientSet: [], + namespaceSetStr: e.state.server.namespaceSetStr, + belongToApp: true, + }; + if (!tmpMap.has(ip)) { + tmpMap.set(ip, group); + } + }); + tmpClientList.forEach((e) => { + let ip = e.ip; + let machineId = ip + '@' + e.commandPort; + + let targetServer = e.state.client.clientConfig.serverHost; + let targetPort = e.state.client.clientConfig.serverPort; + if (targetServer === undefined || targetServer === '' || + targetPort === undefined || targetPort <= 0) { + $scope.remainingClientAddressList.push(generateMachineId(e)); + return; + } + + if (!tmpMap.has(targetServer)) { + let group = { + ip: targetServer, + machineId: targetServer, + port: targetPort, + clientSet: [machineId], + belongToApp: false, + }; + tmpMap.set(targetServer, group); + } else { + let g = tmpMap.get(targetServer); + g.clientSet.push(machineId); + } + }); + tmpMap.forEach((v) => { + if (v !== undefined) { + $scope.clusterMap.push(v); + } + }); + } + + $scope.onCurrentServerChange = () => { + resetChosen(); + }; + + $scope.remainingClientAddressList = []; + + $scope.moveToServerGroup = () => { + let chosenServer = $scope.tmp.curChosenServer; + if (!chosenServer || !chosenServer.machineId) { + return; + } + $scope.tmp.curRemainingClientChosen.forEach(e => { + chosenServer.clientSet.push(e); + removeFromArr($scope.remainingClientAddressList, e); + }); + resetChosen(); + }; + + $scope.moveToRemainingSharePool = () => { + $scope.tmp.curClientChosen.forEach(e => { + $scope.remainingClientAddressList.push(e); + removeFromArr($scope.tmp.curChosenServer.clientSet, e); + }); + resetChosen(); + }; + + function parseIpFromMachineId(machineId) { + if (machineId.indexOf('@') === -1) { + return machineId; + } + let arr = machineId.split('@'); + return arr[0]; + } + + $scope.addToServerList = () => { + let group; + $scope.tmp.curRemainingClientChosen.forEach(e => { + group = { + machineId: e, + ip: parseIpFromMachineId(e), + port: DEFAULT_CLUSTER_SERVER_PORT, + clientSet: [], + namespaceSetStr: 'default,' + $scope.app, + belongToApp: true, + }; + $scope.clusterMap.push(group); + removeFromArr($scope.remainingClientAddressList, e); + $scope.tmp.curChosenServer = group; + }); + resetChosen(); + }; + + $scope.removeFromServerList = () => { + let chosenServer = $scope.tmp.curChosenServer; + if (!chosenServer || !chosenServer.machineId) { + return; + } + chosenServer.clientSet.forEach((e) => { + if (e !== undefined) { + $scope.remainingClientAddressList.push(e); + } + }); + + if (chosenServer.belongToApp || chosenServer.machineId.indexOf('@') !== -1) { + $scope.remainingClientAddressList.push(chosenServer.machineId); + } else { + alert('提示:非本应用内机器将不会置回空闲列表中'); + } + + removeFromArr($scope.clusterMap, chosenServer); + + resetChosen(); + + if ($scope.clusterMap.length > 0) { + $scope.tmp.curChosenServer = $scope.clusterMap[0]; + $scope.onCurrentServerChange(); + } else { + $scope.tmp.curChosenServer = {}; + } + }; + + function retrieveClusterAppInfo() { + ClusterStateService.fetchClusterUniversalStateOfApp($scope.app).success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.appClusterMachineList = data.data; + $scope.appClusterMachineList.forEach(processAppSingleData); + applyClusterMap($scope.appClusterMachineList); + if ($scope.clusterMap.length > 0) { + $scope.tmp.curChosenServer = $scope.clusterMap[0]; + $scope.onCurrentServerChange(); + } + } else { + $scope.appClusterMachineList = {}; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }).error(() => { + $scope.loadError = {message: '未知错误'}; + }); + } + + retrieveClusterAppInfo(); + + $scope.saveAndApplyAssign = () => { + let ok = confirm('是否确认执行变更?'); + if (!ok) { + return; + } + let cm = $scope.clusterMap; + if (!cm) { + cm = []; + } + cm.forEach((e) => { + e.namespaceSet = convertStrToNamespaceSet(e.namespaceSetStr); + }); + cm.namespaceSet = convertStrToNamespaceSet(cm.namespaceSetStr); + let request = { + clusterMap: cm, + remainingList: $scope.remainingClientAddressList, + }; + ClusterStateService.applyClusterFullAssignOfApp($scope.app, request).success((data) => { + if (data.code === 0 && data.data) { + let failedServerSet = data.data.failedServerSet; + let failedClientSet = data.data.failedClientSet; + if (failedClientSet.length === 0 && failedServerSet.length === 0) { + alert('全部推送成功'); + } else { + alert('推送完毕。token server 失败列表:' + JSON.stringify(failedServerSet) + + '; token client 失败列表:' + JSON.stringify(failedClientSet)); + } + + retrieveClusterAppInfo(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('推送失败:' + data.msg); + } + } + }).error(() => { + alert('未知错误'); + }); + }; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_monitor.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_monitor.js new file mode 100644 index 0000000..202fca1 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_server_monitor.js @@ -0,0 +1,97 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SentinelClusterAppServerMonitorController', ['$scope', '$stateParams', 'ngDialog', + 'MachineService', 'ClusterStateService', + function ($scope, $stateParams, ngDialog, MachineService, ClusterStateService) { + $scope.app = $stateParams.app; + const UNSUPPORTED_CODE = 4041; + + const CLUSTER_MODE_SERVER = 1; + + $scope.tmp = { + curChosenServer: {}, + }; + + function convertSetToString(set) { + if (set === undefined) { + return ''; + } + let s = ''; + for (let i = 0; i < set.length; i++) { + s = s + set[i]; + if (i < set.length - 1) { + s = s + ','; + } + } + return s; + } + + function processServerData(serverVO) { + if (serverVO.state && serverVO.state.namespaceSet) { + serverVO.state.namespaceSetStr = convertSetToString(serverVO.state.namespaceSet); + } + } + + $scope.generateConnectionSet = (data) => { + let connectionSet = data; + let s = ''; + if (connectionSet) { + s = s + '['; + for (let i = 0; i < connectionSet.length; i++) { + s = s + connectionSet[i].address; + if (i < connectionSet.length - 1) { + s = s + ', '; + } + } + s = s + ']'; + } else { + s = '[]'; + } + return s; + }; + + $scope.onChosenServerChange = () => { + + }; + + function retrieveClusterServerInfo() { + ClusterStateService.fetchClusterServerStateOfApp($scope.app).success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.serverVOList = data.data; + $scope.serverVOList.forEach(processServerData); + + if ($scope.serverVOList.length > 0) { + $scope.tmp.curChosenServer = $scope.serverVOList[0]; + $scope.onChosenServerChange(); + } + } else { + $scope.serverVOList = {}; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }).error(() => { + $scope.loadError = {message: '未知错误'}; + }); + } + + retrieveClusterServerInfo(); + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_token_client_list.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_token_client_list.js new file mode 100644 index 0000000..177161b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_app_token_client_list.js @@ -0,0 +1,121 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SentinelClusterAppTokenClientListController', ['$scope', '$stateParams', 'ngDialog', + 'MachineService', 'ClusterStateService', + function ($scope, $stateParams, ngDialog, MachineService, ClusterStateService) { + $scope.app = $stateParams.app; + + const UNSUPPORTED_CODE = 4041; + const CLUSTER_MODE_CLIENT = 0; + const CLUSTER_MODE_SERVER = 1; + + function processClientData(clientVO) { + + } + + $scope.modifyClientConfigDialog = (clientVO) => { + if (!clientVO) { + return; + } + $scope.ccDialogData = { + ip: clientVO.ip, + commandPort: clientVO.commandPort, + clientId: clientVO.id, + serverHost: clientVO.state.clientConfig.serverHost, + serverPort: clientVO.state.clientConfig.serverPort, + requestTimeout: clientVO.state.clientConfig.requestTimeout, + }; + $scope.ccDialog = ngDialog.open({ + template: '/app/views/dialog/cluster/cluster-client-config-dialog.html', + width: 700, + overlay: true, + scope: $scope + }); + }; + + function checkValidClientConfig(config) { + if (!config.serverHost || config.serverHost.trim() == '') { + alert('请输入有效的 Token Server IP'); + return false; + } + if (config.serverPort === undefined || config.serverPort <= 0 || config.serverPort > 65535) { + alert('请输入有效的 Token Server 端口'); + return false; + } + if (config.requestTimeout === undefined || config.requestTimeout <= 0) { + alert('请输入有效的请求超时时长'); + return false; + } + return true; + } + + $scope.doModifyClientConfig = () => { + if (!checkValidClientConfig($scope.ccDialogData)) { + return; + } + let id = $scope.ccDialogData.id; + let request = { + app: $scope.app, + ip: $scope.ccDialogData.ip, + port: $scope.ccDialogData.commandPort, + mode: CLUSTER_MODE_CLIENT, + clientConfig: { + serverHost: $scope.ccDialogData.serverHost, + serverPort: $scope.ccDialogData.serverPort, + requestTimeout: $scope.ccDialogData.requestTimeout, + } + }; + ClusterStateService.modifyClusterConfig(request).success((data) => { + if (data.code === 0 && data.data) { + alert('修改 Token Client 配置成功'); + window.location.reload(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('机器 ' + id + ' 的 Sentinel 没有引入集群限流客户端,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('修改失败:' + data.msg); + } + } + }).error((data, header, config, status) => { + alert('未知错误'); + }); + }; + + function retrieveClusterTokenClientInfo() { + ClusterStateService.fetchClusterClientStateOfApp($scope.app) + .success((data) => { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.clientVOList = data.data; + $scope.clientVOList.forEach(processClientData); + } else { + $scope.clientVOList = []; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '该应用的 Sentinel 客户端不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }) + .error(() => { + $scope.loadError = {message: '未知错误'}; + }); + } + + retrieveClusterTokenClientInfo(); + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_single.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_single.js new file mode 100644 index 0000000..7392229 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/cluster_single.js @@ -0,0 +1,262 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SentinelClusterSingleController', ['$scope', '$stateParams', 'ngDialog', + 'MachineService', 'ClusterStateService', + function ($scope, $stateParams, ngDialog, MachineService, ClusterStateService) { + $scope.app = $stateParams.app; + const UNSUPPORTED_CODE = 4041; + + const CLUSTER_MODE_CLIENT = 0; + const CLUSTER_MODE_SERVER = 1; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + function convertSetToString(set) { + if (set === undefined) { + return ''; + } + let s = ''; + for (let i = 0; i < set.length; i++) { + s = s + set[i]; + if (i < set.length - 1) { + s = s + ','; + } + } + return s; + } + + function convertStrToNamespaceSet(str) { + if (str === undefined || str === '') { + return []; + } + let arr = []; + let spliced = str.split(','); + spliced.forEach((v) => { + arr.push(v.trim()); + }); + return arr; + } + + function fetchMachineClusterState() { + if (!$scope.macInputModel || $scope.macInputModel === '') { + return; + } + let mac = $scope.macInputModel.split(':'); + ClusterStateService.fetchClusterUniversalStateSingle($scope.app, mac[0], mac[1]).success(function (data) { + if (data.code == 0 && data.data) { + $scope.loadError = undefined; + $scope.stateVO = data.data; + $scope.stateVO.currentMode = $scope.stateVO.stateInfo.mode; + if ($scope.stateVO.server && $scope.stateVO.server.namespaceSet) { + $scope.stateVO.server.namespaceSetStr = convertSetToString($scope.stateVO.server.namespaceSet); + } + } else { + $scope.stateVO = {}; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: '机器 ' + mac[0] + ':' + mac[1] + ' 的 Sentinel 客户端版本不支持集群限流,请升级至 1.4.0 以上版本并引入相关依赖。'} + } else { + $scope.loadError = {message: data.msg}; + } + } + }).error((data, header, config, status) => { + $scope.loadError = {message: '未知错误'}; + }); + } + + fetchMachineClusterState(); + + function checkValidClientConfig(stateVO) { + if (!stateVO.client || !stateVO.client.clientConfig) { + alert('不合法的配置'); + return false; + } + let config = stateVO.client.clientConfig; + if (!config.serverHost || config.serverHost.trim() == '') { + alert('请输入有效的 Token Server IP'); + return false; + } + if (config.serverPort === undefined || config.serverPort <= 0 || config.serverPort > 65535) { + alert('请输入有效的 Token Server 端口'); + return false; + } + if (config.requestTimeout === undefined || config.requestTimeout <= 0) { + alert('请输入有效的请求超时时长'); + return false; + } + return true; + } + + function sendClusterClientRequest(stateVO) { + if (!checkValidClientConfig(stateVO)) { + return; + } + if (!$scope.macInputModel) { + return; + } + let mac = $scope.macInputModel.split(':'); + let request = { + app: $scope.app, + ip: mac[0], + port: mac[1], + }; + request.mode = CLUSTER_MODE_CLIENT; + request.clientConfig = stateVO.client.clientConfig; + ClusterStateService.modifyClusterConfig(request).success(function (data) { + if (data.code == 0 && data.data) { + alert('修改集群限流客户端配置成功'); + window.location.reload(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('机器 ' + mac[0] + ':' + mac[1] + ' 的 Sentinel 客户端版本不支持集群限流客户端,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('修改失败:' + data.msg); + } + } + }).error((data, header, config, status) => { + alert('未知错误'); + }); + } + + function checkValidServerConfig(stateVO) { + if (!stateVO.server || !stateVO.server.transport) { + alert('不合法的配置'); + return false; + } + if (stateVO.server.namespaceSetStr === undefined || stateVO.server.namespaceSetStr == '') { + alert('请输入有效的命名空间集合(多个 namespace 以 , 分隔)'); + return false; + } + let transportConfig = stateVO.server.transport; + if (transportConfig.port === undefined || transportConfig.port <= 0 || transportConfig.port > 65535) { + alert('请输入有效的 Token Server 端口'); + return false; + } + let flowConfig = stateVO.server.flow; + if (flowConfig.maxAllowedQps === undefined || flowConfig.maxAllowedQps < 0) { + alert('请输入有效的最大允许 QPS'); + return false; + } + // if (transportConfig.idleSeconds === undefined || transportConfig.idleSeconds <= 0) { + // alert('请输入有效的连接清理时长 (idleSeconds)'); + // return false; + // } + return true; + } + + function sendClusterServerRequest(stateVO) { + if (!checkValidServerConfig(stateVO)) { + return; + } + if (!$scope.macInputModel) { + return; + } + let mac = $scope.macInputModel.split(':'); + let request = { + app: $scope.app, + ip: mac[0], + port: mac[1], + }; + request.mode = CLUSTER_MODE_SERVER; + request.flowConfig = stateVO.server.flow; + request.transportConfig = stateVO.server.transport; + request.namespaceSet = convertStrToNamespaceSet(stateVO.server.namespaceSetStr); + ClusterStateService.modifyClusterConfig(request).success(function (data) { + if (data.code == 0 && data.data) { + alert('修改集群限流服务端配置成功'); + window.location.reload(); + } else { + if (data.code === UNSUPPORTED_CODE) { + alert('机器 ' + mac[0] + ':' + mac[1] + ' 的 Sentinel 客户端版本不支持集群限流服务端,请升级至 1.4.0 以上版本并引入相关依赖。'); + } else { + alert('修改失败:' + data.msg); + } + } + }).error((data, header, config, status) => { + alert('未知错误'); + }); + } + + + $scope.saveConfig = () => { + let ok = confirm('是否确定修改集群限流配置?'); + if (!ok) { + return; + } + let mode = $scope.stateVO.stateInfo.mode; + if (mode != 1 && mode != 0) { + alert('未知的集群限流模式'); + return; + } + if (mode == 0) { + sendClusterClientRequest($scope.stateVO); + } else { + sendClusterServerRequest($scope.stateVO); + } + }; + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code === 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptionsOrigin = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptionsOrigin.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + $scope.macsInputOptions = $scope.macsInputOptionsOrigin; + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + } + queryAppMachines(); + + $scope.$watch('searchKey', function () { + if (!$scope.macsInputOptions) { + return; + } + if ($scope.searchKey) { + $scope.macsInputOptions = $scope.macsInputOptionsOrigin + .filter((e) => e.value.indexOf($scope.searchKey) !== -1); + } else { + $scope.macsInputOptions = $scope.macsInputOptionsOrigin; + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } else { + $scope.macInputModel = ''; + } + }); + + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + fetchMachineClusterState(); + } + }); + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/degrade.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/degrade.js new file mode 100644 index 0000000..d2eac6d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/degrade.js @@ -0,0 +1,204 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('DegradeCtl', ['$scope', '$stateParams', 'DegradeService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, DegradeService, ngDialog, MachineService) { + //初始化 + $scope.app = $stateParams.app; + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + DegradeService.queryMachineRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + var degradeRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.degradeRuleDialog = { + title: '编辑降级规则', + type: 'edit', + confirmBtnText: '保存' + }; + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + limitApp: 'default', + minRequestAmount: 5, + statIntervalMs: 1000, + }; + $scope.degradeRuleDialog = { + title: '新增降级规则', + type: 'add', + confirmBtnText: '新增' + }; + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if (!DegradeService.checkRuleValid($scope.currentRule)) { + return; + } + if ($scope.degradeRuleDialog.type === 'add') { + addNewRule($scope.currentRule); + } else if ($scope.degradeRuleDialog.type === 'edit') { + saveRule($scope.currentRule, true); + } + }; + + function parseDegradeMode(grade) { + switch (grade) { + case 0: + return '慢调用比例'; + case 1: + return '异常比例'; + case 2: + return '异常数'; + default: + return '未知'; + } + } + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除降级规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下降级规则', + attention: '资源名: ' + rule.resource + + ', 降级模式: ' + parseDegradeMode(rule.grade) + ', 阈值: ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + DegradeService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('失败:' + data.msg); + } + }); + }; + + function addNewRule(rule) { + DegradeService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + degradeRuleDialog.close(); + } else { + alert('失败:' + data.msg); + } + }); + }; + + function saveRule(rule, edit) { + DegradeService.saveRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + if (edit) { + degradeRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('失败:' + data.msg); + } + }); + } + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v1.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v1.js new file mode 100644 index 0000000..3c64493 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v1.js @@ -0,0 +1,220 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('FlowControllerV1', ['$scope', '$stateParams', 'FlowServiceV1', 'ngDialog', + 'MachineService', + function ($scope, $stateParams, FlowService, ngDialog, + MachineService) { + $scope.app = $stateParams.app; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + $scope.generateThresholdTypeShow = (rule) => { + if (!rule.clusterMode) { + return '单机'; + } + if (rule.clusterConfig.thresholdType === 0) { + return '集群均摊'; + } else if (rule.clusterConfig.thresholdType === 1) { + return '集群总体'; + } else { + return '集群'; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + FlowService.queryMachineRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + var flowRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.flowRuleDialog = { + title: '编辑流控规则', + type: 'edit', + confirmBtnText: '保存', + showAdvanceButton: rule.controlBehavior == 0 && rule.strategy == 0 + }; + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 1, + strategy: 0, + controlBehavior: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + limitApp: 'default', + clusterMode: false, + clusterConfig: { + thresholdType: 0 + } + }; + $scope.flowRuleDialog = { + title: '新增流控规则', + type: 'add', + confirmBtnText: '新增', + showAdvanceButton: true, + }; + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if (!FlowService.checkRuleValid($scope.currentRule)) { + return; + } + if ($scope.flowRuleDialog.type === 'add') { + addNewRule($scope.currentRule); + } else if ($scope.flowRuleDialog.type === 'edit') { + saveRule($scope.currentRule, true); + } + }; + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除流控规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下流控规则', + attention: '资源名: ' + rule.resource + ', 流控应用: ' + rule.limitApp + + ', 阈值类型: ' + (rule.grade == 0 ? '线程数' : 'QPS') + ', 阈值: ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type === 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + FlowService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('失败:' + data.msg); + } + }); + }; + + function addNewRule(rule) { + FlowService.newRule(rule).success(function (data) { + if (data.code === 0) { + getMachineRules(); + flowRuleDialog.close(); + } else { + alert('失败:' + data.msg); + } + }); + }; + + $scope.onOpenAdvanceClick = function () { + $scope.flowRuleDialog.showAdvanceButton = false; + }; + $scope.onCloseAdvanceClick = function () { + $scope.flowRuleDialog.showAdvanceButton = true; + }; + + function saveRule(rule, edit) { + FlowService.saveRule(rule).success(function (data) { + if (data.code === 0) { + getMachineRules(); + if (edit) { + flowRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('失败:' + data.msg); + } + }); + } + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v2.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v2.js new file mode 100644 index 0000000..3280675 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/flow_v2.js @@ -0,0 +1,221 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('FlowControllerV2', ['$scope', '$stateParams', 'FlowServiceV2', 'ngDialog', + 'MachineService', + function ($scope, $stateParams, FlowService, ngDialog, + MachineService) { + $scope.app = $stateParams.app; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + $scope.generateThresholdTypeShow = (rule) => { + if (!rule.clusterMode) { + return '单机'; + } + if (rule.clusterConfig.thresholdType === 0) { + return '集群均摊'; + } else if (rule.clusterConfig.thresholdType === 1) { + return '集群总体'; + } else { + return '集群'; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + FlowService.queryMachineRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + var flowRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.flowRuleDialog = { + title: '编辑流控规则', + type: 'edit', + confirmBtnText: '保存', + showAdvanceButton: rule.controlBehavior == 0 && rule.strategy == 0 + }; + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 1, + strategy: 0, + controlBehavior: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + limitApp: 'default', + clusterMode: false, + clusterConfig: { + thresholdType: 0, + fallbackToLocalWhenFail: true + } + }; + $scope.flowRuleDialog = { + title: '新增流控规则', + type: 'add', + confirmBtnText: '新增', + showAdvanceButton: true, + }; + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if (!FlowService.checkRuleValid($scope.currentRule)) { + return; + } + if ($scope.flowRuleDialog.type === 'add') { + addNewRule($scope.currentRule); + } else if ($scope.flowRuleDialog.type === 'edit') { + saveRule($scope.currentRule, true); + } + }; + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除流控规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下流控规则', + attention: '资源名: ' + rule.resource + ', 流控应用: ' + rule.limitApp + + ', 阈值类型: ' + (rule.grade == 0 ? '线程数' : 'QPS') + ', 阈值: ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type === 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + FlowService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('失败!'); + } + }); + }; + + function addNewRule(rule) { + FlowService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + flowRuleDialog.close(); + } else { + alert('失败!'); + } + }); + }; + + $scope.onOpenAdvanceClick = function () { + $scope.flowRuleDialog.showAdvanceButton = false; + }; + $scope.onCloseAdvanceClick = function () { + $scope.flowRuleDialog.showAdvanceButton = true; + }; + + function saveRule(rule, edit) { + FlowService.saveRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + if (edit) { + flowRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('失败!'); + } + }); + } + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/api.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/api.js new file mode 100644 index 0000000..ccf2497 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/api.js @@ -0,0 +1,245 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayApiCtl', ['$scope', '$stateParams', 'GatewayApiService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, GatewayApiService, ngDialog, MachineService) { + $scope.app = $stateParams.app; + + $scope.apisPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getApis(); + function getApis() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + // To merge rows for api who has more than one predicateItems, here we build data manually + $scope.apis = []; + + data.data.forEach(function(api) { + api["predicateItems"].forEach(function (item, index) { + var newItem = {}; + newItem["id"] = api["id"]; + newItem["app"] = api["app"]; + newItem["ip"] = api["ip"]; + newItem["port"] = api["port"]; + newItem["apiName"] = api["apiName"]; + newItem["pattern"] = item["pattern"]; + newItem["matchStrategy"] = item["matchStrategy"]; + // The itemSize indicates how many rows to merge, by using rowspan="{{api.itemSize}}" in tag + newItem["itemSize"] = api["predicateItems"].length; + // Mark the flag of first item to zero, indicates the start row to merge + newItem["firstFlag"] = index == 0 ? 0 : 1; + // Still hold the data of predicateItems, in order to bind data in edit dialog html + newItem["predicateItems"] = api["predicateItems"]; + $scope.apis.push(newItem); + }); + }); + + $scope.apisPageConfig.totalCount = data.data.length; + } else { + $scope.apis = []; + $scope.apisPageConfig.totalCount = 0; + } + }); + }; + $scope.getApis = getApis; + + var gatewayApiDialog; + $scope.editApi = function (api) { + $scope.currentApi = angular.copy(api); + $scope.gatewayApiDialog = { + title: '编辑自定义 API', + type: 'edit', + confirmBtnText: '保存' + }; + gatewayApiDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/api-dialog.html', + width: 900, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewApi = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentApi = { + grade: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + predicateItems: [{matchStrategy: 0, pattern: ''}] + }; + $scope.gatewayApiDialog = { + title: '新增自定义 API', + type: 'add', + confirmBtnText: '新增' + }; + gatewayApiDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/api-dialog.html', + width: 900, + overlay: true, + scope: $scope + }); + }; + + $scope.saveApi = function () { + var apiNames = []; + if ($scope.gatewayApiDialog.type === 'add') { + apiNames = $scope.apis.map(function (item, index, array) { + return item["apiName"]; + }).filter(function (item, index, array) { + return array.indexOf(item) === index; + }); + } + + if (!GatewayApiService.checkApiValid($scope.currentApi, apiNames)) { + return; + } + + if ($scope.gatewayApiDialog.type === 'add') { + addNewApi($scope.currentApi); + } else if ($scope.gatewayApiDialog.type === 'edit') { + saveApi($scope.currentApi, true); + } + }; + + function addNewApi(api) { + GatewayApiService.newApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + gatewayApiDialog.close(); + } else { + alert('新增自定义API失败!' + data.msg); + } + }); + }; + + function saveApi(api, edit) { + GatewayApiService.saveApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + if (edit) { + gatewayApiDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改自定义API失败!' + data.msg); + } + }); + }; + + var confirmDialog; + $scope.deleteApi = function (api) { + $scope.currentApi = api; + $scope.confirmDialog = { + title: '删除自定义API', + type: 'delete_api', + attentionTitle: '请确认是否删除如下自定义API', + attention: 'API名称: ' + api.apiName, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_api') { + deleteApi($scope.currentApi); + } else { + console.error('error'); + } + }; + + function deleteApi(api) { + GatewayApiService.deleteApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + confirmDialog.close(); + } else { + alert('删除自定义API失败!' + data.msg); + } + }); + }; + + $scope.addNewMatchPattern = function() { + var total; + if ($scope.currentApi.predicateItems == null) { + $scope.currentApi.predicateItems = []; + total = 0; + } else { + total = $scope.currentApi.predicateItems.length; + } + $scope.currentApi.predicateItems.splice(total + 1, 0, {matchStrategy: 0, pattern: ''}); + }; + + $scope.removeMatchPattern = function($index) { + if ($scope.currentApi.predicateItems.length <= 1) { + // Should never happen since no remove button will display when only one predicateItem. + alert('至少有一个匹配规则'); + return; + } + $scope.currentApi.predicateItems.splice($index, 1); + }; + + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getApis(); + } + }); + }] +); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js new file mode 100644 index 0000000..c492cf9 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js @@ -0,0 +1,251 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayFlowCtl', ['$scope', '$stateParams', 'GatewayFlowService', 'GatewayApiService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, GatewayFlowService, GatewayApiService, ngDialog, MachineService) { + $scope.app = $stateParams.app; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayFlowService.queryRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + getApiNames(); + function getApiNames() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.apiNames = []; + + data.data.forEach(function (api) { + $scope.apiNames.push(api["apiName"]); + }); + } + }); + } + + $scope.intervalUnits = [{val: 0, desc: '秒'}, {val: 1, desc: '分'}, {val: 2, desc: '时'}, {val: 3, desc: '天'}]; + + var gatewayFlowRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.gatewayFlowRuleDialog = { + title: '编辑网关流控规则', + type: 'edit', + confirmBtnText: '保存' + }; + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 1, + app: $scope.app, + ip: mac[0], + port: mac[1], + resourceMode: 0, + interval: 1, + intervalUnit: 0, + controlBehavior: 0, + burst: 0, + maxQueueingTimeoutMs: 0 + }; + + $scope.gatewayFlowRuleDialog = { + title: '新增网关流控规则', + type: 'add', + confirmBtnText: '新增' + }; + + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if (!GatewayFlowService.checkRuleValid($scope.currentRule)) { + return; + } + if ($scope.gatewayFlowRuleDialog.type === 'add') { + addNewRule($scope.currentRule); + } else if ($scope.gatewayFlowRuleDialog.type === 'edit') { + saveRule($scope.currentRule, true); + } + }; + + $scope.useRouteID = function() { + $scope.currentRule.resource = ''; + }; + + $scope.useCustormAPI = function() { + $scope.currentRule.resource = ''; + }; + + $scope.useParamItem = function () { + $scope.currentRule.paramItem = { + parseStrategy: 0, + matchStrategy: 0 + }; + }; + + $scope.notUseParamItem = function () { + $scope.currentRule.paramItem = null; + }; + + $scope.useParamItemVal = function() { + $scope.currentRule.paramItem.pattern = ""; + $scope.currentRule.paramItem.matchStrategy = 0; + }; + + $scope.notUseParamItemVal = function() { + $scope.currentRule.paramItem.pattern = null; + $scope.currentRule.paramItem.matchStrategy = null; + }; + + function addNewRule(rule) { + GatewayFlowService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + gatewayFlowRuleDialog.close(); + } else { + alert('新增网关流控规则失败!' + data.msg); + } + }); + }; + + function saveRule(rule, edit) { + GatewayFlowService.saveRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + if (edit) { + gatewayFlowRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改网关流控规则失败!' + data.msg); + } + }); + }; + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除网关流控规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下规则', + attention: 'API名称: ' + rule.resource + ', ' + (rule.grade == 1 ? 'QPS阈值' : '线程数') + ': ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + GatewayFlowService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('删除网关流控规则失败!' + data.msg); + } + }); + }; + + queryAppMachines(); + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + getApiNames(); + } + }); + }] +); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js new file mode 100644 index 0000000..52871b4 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js @@ -0,0 +1,299 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayIdentityCtl', ['$scope', '$stateParams', 'IdentityService', + 'ngDialog', 'GatewayFlowService', 'GatewayApiService', 'DegradeService', 'MachineService', + '$interval', '$location', '$timeout', + function ($scope, $stateParams, IdentityService, ngDialog, + GatewayFlowService, GatewayApiService, DegradeService, MachineService, $interval, $location, $timeout) { + + $scope.app = $stateParams.app; + + $scope.currentPage = 1; + $scope.pageSize = 16; + $scope.totalPage = 1; + $scope.totalCount = 0; + $scope.identities = []; + + $scope.searchKey = ''; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + $scope.table = null; + + getApiNames(); + function getApiNames() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.apiNames = []; + + data.data.forEach(function (api) { + $scope.apiNames.push(api["apiName"]); + }); + } + }); + } + + var gatewayFlowRuleDialog; + var gatewayFlowRuleDialogScope; + $scope.addNewGatewayFlowRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + gatewayFlowRuleDialogScope = $scope.$new(true); + + gatewayFlowRuleDialogScope.apiNames = $scope.apiNames; + + gatewayFlowRuleDialogScope.intervalUnits = [{val: 0, desc: '秒'}, {val: 1, desc: '分'}, {val: 2, desc: '时'}, {val: 3, desc: '天'}]; + + gatewayFlowRuleDialogScope.currentRule = { + grade: 1, + app: $scope.app, + ip: mac[0], + port: mac[1], + resourceMode: gatewayFlowRuleDialogScope.apiNames.indexOf(resource) == -1 ? 0 : 1, + resource: resource, + interval: 1, + intervalUnit: 0, + controlBehavior: 0, + burst: 0, + maxQueueingTimeoutMs: 0 + }; + + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog = { + title: '新增网关流控规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加', + showAdvanceButton: true + }; + + gatewayFlowRuleDialogScope.useRouteID = function() { + gatewayFlowRuleDialogScope.currentRule.resource = ''; + }; + + gatewayFlowRuleDialogScope.useCustormAPI = function() { + gatewayFlowRuleDialogScope.currentRule.resource = ''; + }; + + gatewayFlowRuleDialogScope.useParamItem = function () { + gatewayFlowRuleDialogScope.currentRule.paramItem = { + parseStrategy: 0, + matchStrategy: 0 + }; + }; + + gatewayFlowRuleDialogScope.notUseParamItem = function () { + gatewayFlowRuleDialogScope.currentRule.paramItem = null; + }; + + gatewayFlowRuleDialogScope.useParamItemVal = function() { + gatewayFlowRuleDialogScope.currentRule.paramItem.pattern = ""; + }; + + gatewayFlowRuleDialogScope.notUseParamItemVal = function() { + gatewayFlowRuleDialogScope.currentRule.paramItem.pattern = null; + }; + + gatewayFlowRuleDialogScope.saveRule = saveGatewayFlowRule; + gatewayFlowRuleDialogScope.saveRuleAndContinue = saveGatewayFlowRuleAndContinue; + gatewayFlowRuleDialogScope.onOpenAdvanceClick = function () { + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog.showAdvanceButton = false; + }; + gatewayFlowRuleDialogScope.onCloseAdvanceClick = function () { + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog.showAdvanceButton = true; + }; + + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: gatewayFlowRuleDialogScope + }); + }; + + function saveGatewayFlowRule() { + if (!GatewayFlowService.checkRuleValid(gatewayFlowRuleDialogScope.currentRule)) { + return; + } + GatewayFlowService.newRule(gatewayFlowRuleDialogScope.currentRule).success(function (data) { + if (data.code === 0) { + gatewayFlowRuleDialog.close(); + let url = '/dashboard/gateway/flow/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }).error((data, header, config, status) => { + alert('未知错误'); + }); + } + + function saveGatewayFlowRuleAndContinue() { + if (!GatewayFlowService.checkRuleValid(gatewayFlowRuleDialogScope.currentRule)) { + return; + } + GatewayFlowService.newRule(gatewayFlowRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + gatewayFlowRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var degradeRuleDialog; + $scope.addNewDegradeRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + degradeRuleDialogScope = $scope.$new(true); + degradeRuleDialogScope.currentRule = { + enable: false, + grade: 0, + strategy: 0, + resource: resource, + limitApp: 'default', + app: $scope.app, + ip: mac[0], + port: mac[1] + }; + + degradeRuleDialogScope.degradeRuleDialog = { + title: '新增降级规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加' + }; + degradeRuleDialogScope.saveRule = saveDegradeRule; + degradeRuleDialogScope.saveRuleAndContinue = saveDegradeRuleAndContinue; + + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: degradeRuleDialogScope + }); + }; + + function saveDegradeRule() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + var url = '/dashboard/degrade/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }); + } + + function saveDegradeRuleAndContinue() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var searchHandler; + $scope.searchChange = function (searchKey) { + $timeout.cancel(searchHandler); + searchHandler = $timeout(function () { + $scope.searchKey = searchKey; + reInitIdentityDatas(); + }, 600); + }; + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code === 0) { + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + } + + // Fetch all machines by current app name. + queryAppMachines(); + + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + reInitIdentityDatas(); + } + }); + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); + + var intervalId; + function reInitIdentityDatas() { + getApiNames(); + queryIdentities(); + }; + + function queryIdentities() { + var mac = $scope.macInputModel.split(':'); + if (mac == null || mac.length < 2) { + return; + } + + IdentityService.fetchClusterNodeOfMachine(mac[0], mac[1], $scope.searchKey).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.identities = data.data; + $scope.totalCount = $scope.identities.length; + } else { + $scope.identities = []; + $scope.totalCount = 0; + } + } + ); + }; + $scope.queryIdentities = queryIdentities; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/home.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/home.js new file mode 100644 index 0000000..1df5862 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/home.js @@ -0,0 +1,11 @@ +/** + * @ngdoc function + * @name sentinelDashboardApp.controller:MainCtrl + * @description + * # MainCtrl + * Controller of the sentinelDashboardApp + */ +angular.module('sentinelDashboardApp') + .controller('HomeCtrl', ['$scope', '$position', function ($scope, $position) { + // do noting + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/identity.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/identity.js new file mode 100644 index 0000000..2a14eb1 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/identity.js @@ -0,0 +1,478 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('IdentityCtl', ['$scope', '$stateParams', 'IdentityService', + 'ngDialog', 'FlowServiceV1', 'DegradeService', 'AuthorityRuleService', 'ParamFlowService', 'MachineService', + '$interval', '$location', '$timeout', + function ($scope, $stateParams, IdentityService, ngDialog, + FlowService, DegradeService, AuthorityRuleService, ParamFlowService, MachineService, $interval, $location, $timeout) { + + $scope.app = $stateParams.app; + + $scope.currentPage = 1; + $scope.pageSize = 16; + $scope.totalPage = 1; + $scope.totalCount = 0; + $scope.identities = []; + // 数据自动刷新频率, 默认10s + var DATA_REFRESH_INTERVAL = 30; + + $scope.isExpand = true; + $scope.searchKey = ''; + $scope.firstExpandAll = false; + $scope.isTreeView = true; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + $scope.table = null; + + var flowRuleDialog; + var flowRuleDialogScope; + $scope.addNewFlowRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + flowRuleDialogScope = $scope.$new(true); + flowRuleDialogScope.currentRule = { + enable: false, + strategy: 0, + grade: 1, + controlBehavior: 0, + resource: resource, + limitApp: 'default', + clusterMode: false, + clusterConfig: { + thresholdType: 0 + }, + app: $scope.app, + ip: mac[0], + port: mac[1] + }; + + flowRuleDialogScope.flowRuleDialog = { + title: '新增流控规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加', + showAdvanceButton: true + }; + // $scope.flowRuleDialog = { + // showAdvanceButton : true + // }; + flowRuleDialogScope.saveRule = saveFlowRule; + flowRuleDialogScope.saveRuleAndContinue = saveFlowRuleAndContinue; + flowRuleDialogScope.onOpenAdvanceClick = function () { + flowRuleDialogScope.flowRuleDialog.showAdvanceButton = false; + }; + flowRuleDialogScope.onCloseAdvanceClick = function () { + flowRuleDialogScope.flowRuleDialog.showAdvanceButton = true; + }; + + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: flowRuleDialogScope + }); + }; + + function saveFlowRule() { + if (!FlowService.checkRuleValid(flowRuleDialogScope.currentRule)) { + return; + } + FlowService.newRule(flowRuleDialogScope.currentRule).success(function (data) { + if (data.code === 0) { + flowRuleDialog.close(); + let url = '/dashboard/flow/' + $scope.app; + $location.path(url); + } else { + alert('失败:' + data.msg); + } + }).error((data, header, config, status) => { + alert('未知错误'); + }); + } + + function saveFlowRuleAndContinue() { + if (!FlowService.checkRuleValid(flowRuleDialogScope.currentRule)) { + return; + } + FlowService.newRule(flowRuleDialogScope.currentRule).success(function (data) { + if (data.code === 0) { + flowRuleDialog.close(); + } else { + alert('失败:' + data.msg); + } + }); + } + + var degradeRuleDialog; + var degradeRuleDialogScope; + $scope.addNewDegradeRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + degradeRuleDialogScope = $scope.$new(true); + degradeRuleDialogScope.currentRule = { + enable: false, + grade: 0, + strategy: 0, + resource: resource, + limitApp: 'default', + minRequestAmount: 5, + statIntervalMs: 1000, + app: $scope.app, + ip: mac[0], + port: mac[1] + }; + + degradeRuleDialogScope.degradeRuleDialog = { + title: '新增降级规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加' + }; + degradeRuleDialogScope.saveRule = saveDegradeRule; + degradeRuleDialogScope.saveRuleAndContinue = saveDegradeRuleAndContinue; + + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: degradeRuleDialogScope + }); + }; + + function saveDegradeRule() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code === 0) { + degradeRuleDialog.close(); + var url = '/dashboard/degrade/' + $scope.app; + $location.path(url); + } else { + alert('失败:' + data.msg); + } + }); + } + + function saveDegradeRuleAndContinue() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code === 0) { + degradeRuleDialog.close(); + } else { + alert('失败:' + data.msg); + } + }); + } + + let authorityRuleDialog; + let authorityRuleDialogScope; + + function saveAuthorityRule() { + let ruleEntity = authorityRuleDialogScope.currentRule; + if (!AuthorityRuleService.checkRuleValid(ruleEntity.rule)) { + return; + } + AuthorityRuleService.addNewRule(ruleEntity).success((data) => { + if (data.success) { + authorityRuleDialog.close(); + let url = '/dashboard/authority/' + $scope.app; + $location.path(url); + } else { + alert('添加规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('添加规则失败:' + data.msg); + } else { + alert("添加规则失败:未知错误"); + } + }); + } + + function saveAuthorityRuleAndContinue() { + let ruleEntity = authorityRuleDialogScope.currentRule; + if (!AuthorityRuleService.checkRuleValid(ruleEntity.rule)) { + return; + } + AuthorityRuleService.addNewRule(ruleEntity).success((data) => { + if (data.success) { + authorityRuleDialog.close(); + } else { + alert('添加规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('添加规则失败:' + data.msg); + } else { + alert("添加规则失败:未知错误"); + } + }); + } + + $scope.addNewAuthorityRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + let mac = $scope.macInputModel.split(':'); + authorityRuleDialogScope = $scope.$new(true); + authorityRuleDialogScope.currentRule = { + app: $scope.app, + ip: mac[0], + port: mac[1], + rule: { + resource: resource, + strategy: 0, + limitApp: '', + } + }; + + authorityRuleDialogScope.authorityRuleDialog = { + title: '新增授权规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加' + }; + authorityRuleDialogScope.saveRule = saveAuthorityRule; + authorityRuleDialogScope.saveRuleAndContinue = saveAuthorityRuleAndContinue; + + authorityRuleDialog = ngDialog.open({ + template: '/app/views/dialog/authority-rule-dialog.html', + width: 680, + overlay: true, + scope: authorityRuleDialogScope + }); + }; + + let paramFlowRuleDialog; + let paramFlowRuleDialogScope; + + function saveParamFlowRule() { + let ruleEntity = paramFlowRuleDialogScope.currentRule; + if (!ParamFlowService.checkRuleValid(ruleEntity.rule)) { + return; + } + ParamFlowService.addNewRule(ruleEntity).success((data) => { + if (data.success) { + paramFlowRuleDialog.close(); + let url = '/dashboard/paramFlow/' + $scope.app; + $location.path(url); + } else { + alert('添加热点规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('添加热点规则失败:' + data.msg); + } else { + alert("添加热点规则失败:未知错误"); + } + }); + } + + function saveParamFlowRuleAndContinue() { + let ruleEntity = paramFlowRuleDialogScope.currentRule; + if (!ParamFlowService.checkRuleValid(ruleEntity.rule)) { + return; + } + ParamFlowService.addNewRule(ruleEntity).success((data) => { + if (data.success) { + paramFlowRuleDialog.close(); + } else { + alert('添加热点规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('添加热点规则失败:' + data.msg); + } else { + alert("添加热点规则失败:未知错误"); + } + }); + } + + $scope.addNewParamFlowRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + let mac = $scope.macInputModel.split(':'); + paramFlowRuleDialogScope = $scope.$new(true); + paramFlowRuleDialogScope.currentRule = { + app: $scope.app, + ip: mac[0], + port: mac[1], + rule: { + resource: resource, + grade: 1, + paramFlowItemList: [], + count: 0, + limitApp: 'default', + controlBehavior: 0, + durationInSec: 1, + burstCount: 0, + maxQueueingTimeMs: 0, + clusterMode: false, + clusterConfig: { + thresholdType: 0, + fallbackToLocalWhenFail: true, + } + } + }; + + paramFlowRuleDialogScope.paramFlowRuleDialog = { + title: '新增热点规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加', + supportAdvanced: false, + showAdvanceButton: true + }; + paramFlowRuleDialogScope.saveRule = saveParamFlowRule; + paramFlowRuleDialogScope.saveRuleAndContinue = saveParamFlowRuleAndContinue; + // paramFlowRuleDialogScope.onOpenAdvanceClick = function () { + // paramFlowRuleDialogScope.paramFlowRuleDialog.showAdvanceButton = false; + // }; + // paramFlowRuleDialogScope.onCloseAdvanceClick = function () { + // paramFlowRuleDialogScope.paramFlowRuleDialog.showAdvanceButton = true; + // }; + + paramFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/param-flow-rule-dialog.html', + width: 680, + overlay: true, + scope: paramFlowRuleDialogScope + }); + }; + + var searchHandler; + $scope.searchChange = function (searchKey) { + $timeout.cancel(searchHandler); + searchHandler = $timeout(function () { + $scope.searchKey = searchKey; + $scope.isExpand = true; + $scope.firstExpandAll = true; + reInitIdentityDatas(); + $scope.firstExpandAll = false; + }, 600); + }; + + $scope.initTreeTable = function () { + // if (!$scope.table) { + com_github_culmat_jsTreeTable.register(window); + $scope.table = window.treeTable($('#identities')); + // } + }; + + $scope.expandAll = function () { + $scope.isExpand = true; + }; + $scope.collapseAll = function () { + $scope.isExpand = false; + }; + $scope.treeView = function () { + $scope.isTreeView = true; + queryIdentities(); + }; + $scope.listView = function () { + $scope.isTreeView = false; + queryIdentities(); + }; + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code === 0) { + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + } + + // Fetch all machines by current app name. + queryAppMachines(); + + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + reInitIdentityDatas(); + } + }); + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); + + var intervalId; + function reInitIdentityDatas() { + // $interval.cancel(intervalId); + queryIdentities(); + // intervalId = $interval(function () { + // queryIdentities(); + // }, DATA_REFRESH_INTERVAL * 1000); + }; + + function queryIdentities() { + var mac = $scope.macInputModel.split(':'); + if (mac == null || mac.length < 2) { + return; + } + if ($scope.isTreeView) { + IdentityService.fetchIdentityOfMachine(mac[0], mac[1], $scope.searchKey).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.identities = data.data; + $scope.totalCount = $scope.identities.length; + } else { + $scope.identities = []; + $scope.totalCount = 0; + } + } + ); + } else { + IdentityService.fetchClusterNodeOfMachine(mac[0], mac[1], $scope.searchKey).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.identities = data.data; + $scope.totalCount = $scope.identities.length; + } else { + $scope.identities = []; + $scope.totalCount = 0; + } + } + ); + } + }; + $scope.queryIdentities = queryIdentities; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/login.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/login.js new file mode 100644 index 0000000..3d49d3c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/login.js @@ -0,0 +1,33 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('LoginCtl', ['$scope', '$state', '$window', 'AuthService', + function ($scope, $state, $window, AuthService) { + // If auth passed, jump to the index page directly + if ($window.localStorage.getItem('session_sentinel_admin')) { + $state.go('dashboard'); + } + + $scope.login = function () { + if (!$scope.username) { + alert('请输入用户名'); + return; + } + + if (!$scope.password) { + alert('请输入密码'); + return; + } + + var param = {"username": $scope.username, "password": $scope.password}; + + AuthService.login(param).success(function (data) { + if (data.code == 0) { + $window.localStorage.setItem('session_sentinel_admin', JSON.stringify(data.data)); + $state.go('dashboard'); + } else { + alert(data.msg); + } + }); + }; + }] +); \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/machine.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/machine.js new file mode 100644 index 0000000..1618047 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/machine.js @@ -0,0 +1,65 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('MachineCtl', ['$scope', '$stateParams', 'MachineService', + function ($scope, $stateParams, MachineService) { + $scope.app = $stateParams.app; + $scope.propertyName = ''; + $scope.reverse = false; + $scope.currentPage = 1; + $scope.machines = []; + $scope.machinesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.sortBy = function (propertyName) { + // console.log('machine sortBy ' + propertyName); + $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false; + $scope.propertyName = propertyName; + }; + + $scope.reloadMachines = function() { + MachineService.getAppMachines($scope.app).success( + function (data) { + // console.log('get machines: ' + data.data[0].hostname) + if (data.code == 0 && data.data) { + $scope.machines = data.data; + var healthy = 0; + $scope.machines.forEach(function (item) { + if (item.healthy) { + healthy++; + } + if (!item.hostname) { + item.hostname = '未知' + } + }) + $scope.healthyCount = healthy; + $scope.machinesPageConfig.totalCount = $scope.machines.length; + } else { + $scope.machines = []; + $scope.healthyCount = 0; + } + } + ); + }; + + $scope.removeMachine = function(ip, port) { + if (!confirm("confirm to remove machine [" + ip + ":" + port + "]?")) { + return; + } + MachineService.removeAppMachine($scope.app, ip, port).success( + function(data) { + if (data.code == 0) { + $scope.reloadMachines(); + } else { + alert("remove failed"); + } + } + ); + }; + + $scope.reloadMachines(); + + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/main.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/main.js new file mode 100644 index 0000000..37500f7 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/main.js @@ -0,0 +1,10 @@ +/** + * @ngdoc function + * @name sentinelDashboardApp.controller:MainCtrl + * @description + * # MainCtrl + * Controller of the sentinelDashboardApp + */ +angular.module('sentinelDashboardApp') + .controller('DashboardCtrl', ['$scope', '$position', function ($scope, $position) { + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/metric.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/metric.js new file mode 100644 index 0000000..08d713f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/metric.js @@ -0,0 +1,268 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('MetricCtl', ['$scope', '$stateParams', 'MetricService', '$interval', '$timeout', + function ($scope, $stateParams, MetricService, $interval, $timeout) { + + $scope.app = $stateParams.app; + // 数据自动刷新频率 + var DATA_REFRESH_INTERVAL = 1000 * 10; + + $scope.servicePageConfig = { + pageSize: 6, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.servicesChartConfigs = []; + // 随时刷新时间 + function formatDate(date) { + return moment(date).format('YYYY/MM/DD HH:mm:ss'); + } + + $scope.pageChanged = function (newPageNumber) { + $scope.servicePageConfig.currentPageIndex = newPageNumber; + reInitIdentityDatas(); + }; + + var searchT; + $scope.searchService = function () { + $timeout.cancel(searchT); + + searchT = $timeout(function () { + reInitIdentityDatas(); + }, 600); + } + + var intervalId; + reInitIdentityDatas(); + function reInitIdentityDatas() { + $interval.cancel(intervalId); + queryIdentityDatas(); + intervalId = $interval(function () { + queryIdentityDatas(); + }, DATA_REFRESH_INTERVAL); + }; + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); + $scope.initAllChart = function () { + $.each($scope.metrics, function (idx, metric) { + if (idx == $scope.metrics.length - 1) { + return; + } + const chart = new G2.Chart({ + container: 'chart' + idx, + forceFit: true, + width: 100, + height: 250, + padding: [10, 30, 70, 50] + }); + var maxQps = 0; + for (var i in metric.data) { + var item = metric.data[i]; + if (item.passQps > maxQps) { + maxQps = item.passQps; + } + if (item.blockQps > maxQps) { + maxQps = item.blockQps; + } + } + chart.source(metric.data); + chart.scale('timestamp', { + type: 'time', + mask: 'YYYY-MM-DD HH:mm:ss' + }); + chart.scale('passQps', { + min: 0, + max: maxQps, + fine: true, + alias: '通过 QPS' + // max: 10 + }); + chart.scale('blockQps', { + min: 0, + max: maxQps, + fine: true, + alias: '拒绝 QPS', + }); + chart.scale('rt', { + min: 0, + fine: true, + }); + chart.axis('rt', { + grid: null, + label: null + }); + chart.axis('blockQps', { + grid: null, + label: null + }); + + chart.axis('timestamp', { + label: { + textStyle: { + textAlign: 'center', // 文本对齐方向,可取值为: start center end + fill: '#404040', // 文本的颜色 + fontSize: '11', // 文本大小 + //textBaseline: 'top', // 文本基准线,可取 top middle bottom,默认为middle + }, + autoRotate: false, + formatter: function (text, item, index) { + return text.substring(11, 11 + 5); + } + } + }); + chart.legend({ + custom: true, + position: 'bottom', + allowAllCanceled: true, + itemFormatter: function (val) { + if ('passQps' === val) { + return '通过 QPS'; + } + if ('blockQps' === val) { + return '拒绝 QPS'; + } + return val; + }, + items: [ + { value: 'passQps', marker: { symbol: 'hyphen', stroke: 'green', radius: 5, lineWidth: 2 } }, + { value: 'blockQps', marker: { symbol: 'hyphen', stroke: 'blue', radius: 5, lineWidth: 2 } }, + //{ value: 'rt', marker: {symbol: 'hyphen', stroke: 'gray', radius: 5, lineWidth: 2} }, + ], + onClick: function (ev) { + const item = ev.item; + const value = item.value; + const checked = ev.checked; + const geoms = chart.getAllGeoms(); + for (var i = 0; i < geoms.length; i++) { + const geom = geoms[i]; + if (geom.getYScale().field === value) { + if (checked) { + geom.show(); + } else { + geom.hide(); + } + } + } + } + }); + chart.line().position('timestamp*passQps').size(1).color('green').shape('smooth'); + chart.line().position('timestamp*blockQps').size(1).color('blue').shape('smooth'); + //chart.line().position('timestamp*rt').size(1).color('gray').shape('smooth'); + G2.track(false); + chart.render(); + }); + }; + $scope.metrics = []; + $scope.emptyObjs = []; + function queryIdentityDatas() { + var now = new Date(); + var endDateTime; + var startDateTime; + if ($scope.endTime !== null && $scope.endTime !== '' && $scope.endTime !== undefined){ + endDateTime = new Date(Date.parse($scope.endTime)); + }else { + endDateTime = now; + } + if ($scope.startTime !== null && $scope.startTime !== '' && $scope.startTime !== undefined){ + startDateTime = new Date(Date.parse($scope.startTime)); + }else { + startDateTime = moment(now).subtract(5, 'minutes').toDate(); + } + var startTimeStamp = startDateTime.getTime(); + var endTimeStamp = endDateTime.getTime(); + console.log(formatDate(startTimeStamp), formatDate(endTimeStamp)); + var params = { + app: $scope.app, + pageIndex: $scope.servicePageConfig.currentPageIndex, + pageSize: $scope.servicePageConfig.pageSize, + desc: $scope.isDescOrder, + searchKey: $scope.serviceQuery, + startTime: startTimeStamp, + endTime: endTimeStamp + }; + MetricService.queryAppSortedIdentities(params).success(function (data) { + $scope.metrics = []; + $scope.emptyObjs = []; + if (data.code === 0 && data.data) { + var metricsObj = data.data.metric; + var identityNames = Object.keys(metricsObj); + if (identityNames.length < 1) { + $scope.emptyServices = true; + } else { + $scope.emptyServices = false; + } + $scope.servicePageConfig.totalPage = data.data.totalPage; + $scope.servicePageConfig.pageSize = data.data.pageSize; + var totalCount = data.data.totalCount; + $scope.servicePageConfig.totalCount = totalCount; + for (i = 0; i < totalCount; i++) { + $scope.emptyObjs.push({}); + } + $.each(identityNames, function (idx, identityName) { + var identityDatas = metricsObj[identityName]; + var metrics = {}; + metrics.resource = identityName; + // metrics.data = identityDatas; + metrics.data = fillZeros(identityDatas); + metrics.shortData = lastOfArray(identityDatas, 6); + $scope.metrics.push(metrics); + }); + // push an empty element in the last, for ng-init reasons. + $scope.metrics.push([]); + } else { + $scope.emptyServices = true; + console.log(data.msg); + } + }); + }; + function fillZeros(metricData) { + if (!metricData || metricData.length == 0) { + return []; + } + var filledData = []; + filledData.push(metricData[0]); + var lastTime = metricData[0].timestamp / 1000; + for (var i = 1; i < metricData.length; i++) { + var curTime = metricData[i].timestamp / 1000; + if (curTime > lastTime + 1) { + for (var j = lastTime + 1; j < curTime; j++) { + filledData.push({ + "timestamp": j * 1000, + "passQps": 0, + "blockQps": 0, + "successQps": 0, + "exception": 0, + "rt": 0, + "count": 0 + }) + } + } + filledData.push(metricData[i]); + lastTime = curTime; + } + return filledData; + } + function lastOfArray(arr, n) { + if (!arr.length) { + return []; + } + var rs = []; + for (i = 0; i < n && i < arr.length; i++) { + rs.push(arr[arr.length - 1 - i]); + } + return rs; + } + + $scope.isDescOrder = true; + $scope.setDescOrder = function () { + $scope.isDescOrder = true; + reInitIdentityDatas(); + } + $scope.setAscOrder = function () { + $scope.isDescOrder = false; + reInitIdentityDatas(); + } + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/param_flow.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/param_flow.js new file mode 100644 index 0000000..65d868a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/param_flow.js @@ -0,0 +1,328 @@ +/** + * Parameter flow control controller. + * + * @author Eric Zhao + */ +angular.module('sentinelDashboardApp').controller('ParamFlowController', ['$scope', '$stateParams', 'ParamFlowService', 'ngDialog', + 'MachineService', + function ($scope, $stateParams, ParamFlowService, ngDialog, + MachineService) { + const UNSUPPORTED_CODE = 4041; + $scope.app = $stateParams.app; + $scope.curExItem = {}; + + $scope.paramItemClassTypeList = [ + 'int', 'double', 'java.lang.String', 'long', 'float', 'char', 'byte' + ]; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + function updateSingleParamItem(arr, v, t, c) { + for (let i = 0; i < arr.length; i++) { + if (arr[i].object === v && arr[i].classType === t) { + arr[i].count = c; + return; + } + } + arr.push({object: v, classType: t, count: c}); + } + + function removeSingleParamItem(arr, v, t) { + for (let i = 0; i < arr.length; i++) { + if (arr[i].object === v && arr[i].classType === t) { + arr.splice(i, 1); + break; + } + } + } + + function isNumberClass(classType) { + return classType === 'int' || classType === 'double' || + classType === 'float' || classType === 'long' || classType === 'short'; + } + + function isByteClass(classType) { + return classType === 'byte'; + } + + function notNumberAtLeastZero(num) { + return num === undefined || num === '' || isNaN(num) || num < 0; + } + + function notGoodNumber(num) { + return num === undefined || num === '' || isNaN(num); + } + + function notGoodNumberBetweenExclusive(num, l ,r) { + return num === undefined || num === '' || isNaN(num) || num < l || num > r; + } + + $scope.notValidParamItem = (curExItem) => { + if (isNumberClass(curExItem.classType) && notGoodNumber(curExItem.object)) { + return true; + } + if (isByteClass(curExItem.classType) && notGoodNumberBetweenExclusive(curExItem.object, -128, 127)) { + return true; + } + return curExItem.object === undefined || curExItem.classType === undefined || + notNumberAtLeastZero(curExItem.count); + }; + + $scope.addParamItem = () => { + updateSingleParamItem($scope.currentRule.rule.paramFlowItemList, + $scope.curExItem.object, $scope.curExItem.classType, $scope.curExItem.count); + let oldItem = $scope.curExItem; + $scope.curExItem = {classType: oldItem.classType}; + }; + + $scope.removeParamItem = (v, t) => { + removeSingleParamItem($scope.currentRule.rule.paramFlowItemList, v, t); + }; + + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + let mac = $scope.macInputModel.split(':'); + ParamFlowService.queryMachineRules($scope.app, mac[0], mac[1]) + .success(function (data) { + if (data.code === 0 && data.data) { + $scope.loadError = undefined; + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + if (data.code === UNSUPPORTED_CODE) { + $scope.loadError = {message: "机器 " + mac[0] + ":" + mac[1] + " 的 Sentinel 客户端版本不支持热点参数限流功能,请升级至 0.2.0 以上版本并引入 sentinel-parameter-flow-control 依赖。"} + } else { + $scope.loadError = {message: data.msg} + } + } + }) + .error((data, header, config, status) => { + $scope.loadError = {message: "未知错误"} + }); + } + $scope.getMachineRules = getMachineRules; + getMachineRules(); + + var paramFlowRuleDialog; + + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + if ($scope.currentRule.rule && $scope.currentRule.rule.durationInSec === undefined) { + $scope.currentRule.rule.durationInSec = 1; + } + $scope.paramFlowRuleDialog = { + title: '编辑热点规则', + type: 'edit', + confirmBtnText: '保存', + supportAdvanced: true, + showAdvanceButton: rule.rule.paramFlowItemList === undefined || rule.rule.paramFlowItemList.length <= 0 + }; + paramFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/param-flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + $scope.curExItem = {}; + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + app: $scope.app, + ip: mac[0], + port: mac[1], + rule: { + grade: 1, + paramFlowItemList: [], + count: 0, + limitApp: 'default', + controlBehavior: 0, + durationInSec: 1, + burstCount: 0, + maxQueueingTimeMs: 0, + clusterMode: false, + clusterConfig: { + thresholdType: 0, + fallbackToLocalWhenFail: true, + } + } + }; + $scope.paramFlowRuleDialog = { + title: '新增热点规则', + type: 'add', + confirmBtnText: '新增', + supportAdvanced: true, + showAdvanceButton: true, + }; + paramFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/param-flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + $scope.curExItem = {}; + }; + + $scope.onOpenAdvanceClick = function () { + $scope.paramFlowRuleDialog.showAdvanceButton = false; + }; + $scope.onCloseAdvanceClick = function () { + $scope.paramFlowRuleDialog.showAdvanceButton = true; + }; + + $scope.saveRule = function () { + if (!ParamFlowService.checkRuleValid($scope.currentRule.rule)) { + return; + } + if ($scope.paramFlowRuleDialog.type === 'add') { + addNewRuleAndPush($scope.currentRule); + } else if ($scope.paramFlowRuleDialog.type === 'edit') { + saveRuleAndPush($scope.currentRule, true); + } + }; + + function addNewRuleAndPush(rule) { + ParamFlowService.addNewRule(rule).success((data) => { + if (data.success) { + getMachineRules(); + paramFlowRuleDialog.close(); + } else { + alert('添加规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('添加规则失败:' + data.msg); + } else { + alert("添加规则失败:未知错误"); + } + }); + } + + function saveRuleAndPush(rule, edit) { + ParamFlowService.saveRule(rule).success(function (data) { + if (data.success) { + alert("修改规则成功"); + getMachineRules(); + if (edit) { + paramFlowRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('修改规则失败:' + data.msg); + } else { + alert("修改规则失败:未知错误"); + } + }); + } + + function deleteRuleAndPush(entity) { + if (entity.id === undefined || isNaN(entity.id)) { + alert('规则 ID 不合法!'); + return; + } + ParamFlowService.deleteRule(entity).success((data) => { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('删除规则失败:' + data.msg); + } + }).error((data) => { + if (data) { + alert('删除规则失败:' + data.msg); + } else { + alert("删除规则失败:未知错误"); + } + }); + }; + + var confirmDialog; + $scope.deleteRule = function (ruleEntity) { + $scope.currentRule = ruleEntity; + console.log('deleting: ' + ruleEntity); + $scope.confirmDialog = { + title: '删除热点规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下热点参数限流规则', + attention: '资源名: ' + ruleEntity.rule.resource + ', 热点参数索引: ' + ruleEntity.rule.paramIdx + + ', 限流模式: ' + (ruleEntity.rule.grade === 1 ? 'QPS' : '未知') + ', 限流阈值: ' + ruleEntity.rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type === 'delete_rule') { + deleteRuleAndPush($scope.currentRule); + } else { + console.error('error'); + } + }; + + queryAppMachines(); + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/system.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/system.js new file mode 100644 index 0000000..5b3107f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/controllers/system.js @@ -0,0 +1,239 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, SystemService, + ngDialog, MachineService) { + //初始化 + $scope.app = $stateParams.app; + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + let mac = $scope.macInputModel.split(':'); + SystemService.queryMachineRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code === 0 && data.data) { + $scope.rules = data.data; + $.each($scope.rules, function (idx, rule) { + if (rule.highestSystemLoad >= 0) { + rule.grade = 0; + } else if (rule.avgRt >= 0) { + rule.grade = 1; + } else if (rule.maxThread >= 0) { + rule.grade = 2; + } else if (rule.qps >= 0) { + rule.grade = 3; + } else if (rule.highestCpuUsage >= 0) { + rule.grade = 4; + } + }); + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + } + + $scope.getMachineRules = getMachineRules; + var systemRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.systemRuleDialog = { + title: '编辑系统保护规则', + type: 'edit', + confirmBtnText: '保存' + }; + systemRuleDialog = ngDialog.open({ + template: '/app/views/dialog/system-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + }; + $scope.systemRuleDialog = { + title: '新增系统保护规则', + type: 'add', + confirmBtnText: '新增' + }; + systemRuleDialog = ngDialog.open({ + template: '/app/views/dialog/system-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if ($scope.systemRuleDialog.type === 'add') { + addNewRule($scope.currentRule); + } else if ($scope.systemRuleDialog.type === 'edit') { + saveRule($scope.currentRule, true); + } + }; + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + var ruleTypeDesc = ''; + var ruleTypeCount = null; + if (rule.highestSystemLoad != -1) { + ruleTypeDesc = 'LOAD'; + ruleTypeCount = rule.highestSystemLoad; + } else if (rule.avgRt != -1) { + ruleTypeDesc = 'RT'; + ruleTypeCount = rule.avgRt; + } else if (rule.maxThread != -1) { + ruleTypeDesc = '线程数'; + ruleTypeCount = rule.maxThread; + } else if (rule.qps != -1) { + ruleTypeDesc = 'QPS'; + ruleTypeCount = rule.qps; + }else if (rule.highestCpuUsage != -1) { + ruleTypeDesc = 'CPU 使用率'; + ruleTypeCount = rule.highestCpuUsage; + } + + $scope.confirmDialog = { + title: '删除系统保护规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下系统保护规则', + attention: '阈值类型: ' + ruleTypeDesc + ', 阈值: ' + ruleTypeCount, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + + $scope.confirm = function () { + if ($scope.confirmDialog.type === 'delete_rule') { + deleteRule($scope.currentRule); + // } else if ($scope.confirmDialog.type == 'enable_rule') { + // $scope.currentRule.enable = true; + // saveRule($scope.currentRule); + // } else if ($scope.confirmDialog.type == 'disable_rule') { + // $scope.currentRule.enable = false; + // saveRule($scope.currentRule); + // } else if ($scope.confirmDialog.type == 'enable_all') { + // enableAll($scope.app); + // } else if ($scope.confirmDialog.type == 'disable_all') { + // disableAll($scope.app); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + SystemService.deleteRule(rule).success(function (data) { + if (data.code === 0) { + getMachineRules(); + confirmDialog.close(); + } else if (data.msg != null) { + alert('失败:' + data.msg); + } else { + alert('失败:未知错误'); + } + }); + } + + function addNewRule(rule) { + if (rule.grade == 4 && (rule.highestCpuUsage < 0 || rule.highestCpuUsage > 1)) { + alert('CPU 使用率模式的取值范围应为 [0.0, 1.0],对应 0% - 100%'); + return; + } + SystemService.newRule(rule).success(function (data) { + if (data.code === 0) { + getMachineRules(); + systemRuleDialog.close(); + } else if (data.msg != null) { + alert('失败:' + data.msg); + } else { + alert('失败:未知错误'); + } + }); + } + + function saveRule(rule, edit) { + SystemService.saveRule(rule).success(function (data) { + if (data.code === 0) { + getMachineRules(); + if (edit) { + systemRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else if (data.msg != null) { + alert('失败:' + data.msg); + } else { + alert('失败:未知错误'); + } + }); + } + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code === 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.html new file mode 100644 index 0000000..4584e89 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.html @@ -0,0 +1,15 @@ +
+ + + +
\ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.js new file mode 100644 index 0000000..4e6c8f2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/header/header.js @@ -0,0 +1,61 @@ +/** + * @ngdoc directive + * @name izzyposWebApp.directive:adminPosHeader + * @description + * # adminPosHeader + */ +angular.module('sentinelDashboardApp') + .directive('header', ['VersionService', 'AuthService', function () { + return { + templateUrl: 'app/scripts/directives/header/header.html', + restrict: 'E', + replace: true, + controller: function ($scope, $state, $window, VersionService, AuthService) { + VersionService.version().success(function (data) { + if (data.code == 0) { + $scope.dashboardVersion = data.data; + } + }); + + if (!$window.localStorage.getItem("session_sentinel_admin")) { + AuthService.check().success(function (data) { + if (data.code == 0) { + $window.localStorage.setItem('session_sentinel_admin', JSON.stringify(data.data)); + handleLogout($scope, data.data.id) + } else { + $state.go('login'); + } + }); + } else { + try { + var id = JSON.parse($window.localStorage.getItem("session_sentinel_admin")).id; + handleLogout($scope, id); + } catch (e) { + // Historical version compatibility processing, fixes issue-1449 + // If error happens while parsing, remove item in localStorage and redirect to login page. + $window.localStorage.removeItem("session_sentinel_admin"); + $state.go('login'); + } + } + + function handleLogout($scope, id) { + if (id == 'FAKE_EMP_ID') { + $scope.showLogout = false; + } else { + $scope.showLogout = true; + } + } + + $scope.logout = function () { + AuthService.logout().success(function (data) { + if (data.code == 0) { + $window.localStorage.removeItem("session_sentinel_admin"); + $state.go('login'); + } else { + alert('logout error'); + } + }); + } + } + } + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html new file mode 100644 index 0000000..18b2b3a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js new file mode 100644 index 0000000..31acca6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js @@ -0,0 +1,20 @@ +/** + * @ngdoc directive + * @name izzyposWebApp.directive:adminPosHeader + * @description + * # adminPosHeader + */ + +angular.module('sentinelDashboardApp') + .directive('sidebarSearch', function () { + return { + templateUrl: 'app/scripts/directives/sidebar/sidebar-search/sidebar-search.html', + restrict: 'E', + replace: true, + scope: { + }, + controller: function ($scope) { + $scope.selectedMenu = 'home'; + } + } + }); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html new file mode 100644 index 0000000..e85ba8a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html @@ -0,0 +1,91 @@ + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js new file mode 100644 index 0000000..7ef5740 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js @@ -0,0 +1,71 @@ +angular.module('sentinelDashboardApp') + .directive('sidebar', ['$location', '$stateParams', 'AppService', function () { + return { + templateUrl: 'app/scripts/directives/sidebar/sidebar.html', + restrict: 'E', + replace: true, + scope: { + }, + controller: function ($scope, $stateParams, $location, AppService) { + $scope.app = $stateParams.app; + $scope.collapseVar = 0; + + // app + AppService.getApps().success( + function (data) { + if (data.code === 0) { + let path = $location.path().split('/'); + let initHashApp = path[path.length - 1]; + $scope.apps = data.data; + $scope.apps = $scope.apps.map(function (item) { + if (item.app === initHashApp) { + item.active = true; + } + let healthyCount = 0; + for (let i in item.machines) { + if (item.machines[i].healthy) { + healthyCount++; + } + } + item.healthyCount = healthyCount; + // Handle appType + item.isGateway = item.appType === 1 || item.appType === 11 || item.appType === 12; + + if (item.shown) { + return item; + } + }); + } + } + ); + + // toggle side bar + $scope.click = function ($event) { + let entry = angular.element($event.target).scope().entry; + entry.active = !entry.active;// toggle this clicked app bar + + $scope.apps.forEach(function (item) { // collapse other app bars + if (item !== entry) { + item.active = false; + } + }); + }; + + /** + * @deprecated + */ + $scope.addSearchApp = function () { + let findApp = false; + for (let i = 0; i < $scope.apps.length; i++) { + if ($scope.apps[i].app === $scope.searchApp) { + findApp = true; + break; + } + } + if (!findApp) { + $scope.apps.push({ app: $scope.searchApp }); + } + }; + } + }; + }]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/filters/filters.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/filters/filters.js new file mode 100644 index 0000000..f39b08f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/filters/filters.js @@ -0,0 +1,17 @@ +var app = angular.module('sentinelDashboardApp'); + +app.filter('range', [function () { + return function (input, length) { + if (isNaN(length) || length <= 0) { + return []; + } + + input = []; + for (var index = 1; index <= length; index++) { + input.push(index); + } + + return input; + }; + +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/libs/treeTable.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/libs/treeTable.js new file mode 100644 index 0000000..1eff197 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/libs/treeTable.js @@ -0,0 +1,292 @@ +var com_github_culmat_jsTreeTable = (function(){ + + function depthFirst(tree, func, childrenAttr) { + childrenAttr = childrenAttr || 'children' + function i_depthFirst(node) { + if (node[childrenAttr]) { + $.each(node[childrenAttr], function(i, child) { + i_depthFirst(child) + }) + } + func(node) + } + $.each(tree, function(i, root) { + i_depthFirst(root) + }) + return tree + } + + /* + * make a deep copy of the object + */ + function copy(data){ + return JSON.parse(JSON.stringify(data)) + } + + function makeTree (data, idAttr, refAttr, childrenAttr) { + var data_tmp = data + idAttr = idAttr || 'id' + refAttr = refAttr || 'parent' + childrenAttr = childrenAttr || 'children' + + var byName = [] + $.each(data_tmp, function(i, entry) { + byName[entry[idAttr]] = entry + }) + var tree = [] + $.each(data_tmp, function(i, entry) { + var parents = entry[refAttr] + if(!$.isArray(parents)){ + parents = [parents] + } + if(parents.length == 0){ + tree.push(entry) + } else { + var inTree = false; + $.each(parents, function(i,parentID){ + var parent = byName[parentID] + if (parent) { + if (!parent[childrenAttr]) { + parent[childrenAttr] = [] + } + if($.inArray(entry, parent[childrenAttr])< 0) + parent[childrenAttr].push(entry) + inTree = true + } + }) + if(!inTree){ + tree.push(entry) + } + } + }) + return tree + } + + function renderTree(tree, childrenAttr, idAttr, attrs, renderer, tableAttributes) { + childrenAttr = childrenAttr || 'children' + idAttr = idAttr || 'id' + tableAttributes = tableAttributes || {} + var maxLevel = 0; + var ret = [] + + var table = $("") + $.each(tableAttributes, function(key, value){ + if(key == 'class' && value != 'jsTT') { + table.addClass(value) + } else { + table.attr(key, value) + } + }) + var thead = $("") + var tr = $("") + var tbody = $("") + + table.append(thead) + thead.append(tr) + table.append(tbody) + if (attrs) { + $.each(attrs, function(attr, desc) { + $(tr).append($('')) + }) + } else { + $(tr).append($('')) + $.each(tree[0], function(key, value) { + if (key != childrenAttr && key != idAttr) + $(tr).append($('')) + }) + } + + function render(node, parent) { + var tr = $("") + $(tr).attr('data-tt-id', node[idAttr]) + $(tr).attr('data-tt-level', node['data-tt-level']) + if(!node[childrenAttr] || node[childrenAttr].length == 0) + $(tr).attr('data-tt-isleaf', true) + else + $(tr).attr('data-tt-isnode', true) + if (parent) { + $(tr).attr('data-tt-parent-id', parent[idAttr]) + } + if (renderer) { + renderer($(tr), node) + }else if (attrs) { + $.each(attrs, function(attr, desc) { + $(tr).append($('')) + }) + } else { + $(tr).append($('')) + $.each(node, function(key, value) { + if (key != childrenAttr && key != idAttr && key != 'data-tt-level') + $(tr).append($('')) + }) + } + tbody.append(tr) + } + + function i_renderTree(subTree, childrenAttr, level, parent) { + maxLevel = Math.max(maxLevel, level) + $.each(subTree, function(i, node) { + node['data-tt-level'] = level + render(node, parent) + if (node[childrenAttr]) { + $.each(node[childrenAttr], function(i, child) { + i_renderTree([ child ], childrenAttr, level + 1, node) + }) + } + }) + } + i_renderTree(tree, childrenAttr, 1) + if (tree[0]) + tree[0].maxLevel = maxLevel + return table + } + + function attr2attr(nodes, attrs){ + $.each(nodes, function(i, node) { + $.each(attrs, function(j, at) { + node[at] = $(node).attr(at) + }) + }) + return nodes + } + + function treeTable(table){ + table.addClass('jsTT') + table.expandLevel = function (n) { + $("tr[data-tt-level]", table).each(function(index) { + var level = parseInt($(this).attr('data-tt-level')) + if (level > n-1) { + this.trCollapse(true) + } else if (level == n-1){ + this.trExpand(true) + } + }) + } + function getLevel(node){ + var level = node.attr('data-tt-level') + if(level != undefined ) return parseInt(level) + var parentID = node.attr('data-tt-parent-id') + if( parentID == undefined){ + return 0 + } else { + return getLevel($('tr[data-tt-id="'+parentID+'"]', table).first()) + 1 + } + } + $("tr[data-tt-id]", table).each(function(i,node){ + node = $(node) + node.attr('data-tt-level', getLevel(node)) + }) + var dat = $("tr[data-tt-level]", table).get() + $.each(dat, function(j, d) { + d.trChildrenVisible = true + d.trChildren = [] + }) + dat = attr2attr(dat, ['data-tt-id', 'data-tt-parent-id']) + dat = makeTree(dat, 'data-tt-id', 'data-tt-parent-id', 'trChildren') + + var imgExpand = "data:image/png;base64,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" + var imgCollapse = "data:image/png;base64,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" + $("tr[data-tt-level]", table).each(function(index, tr) { + var level = $(tr).attr('data-tt-level') + var td = $("td",tr).first() + if(tr.trChildren.length>0){ + td.prepend($('')) + } else { + td.prepend($('')) + } + td.prepend($('')) + // td.css('white-space','nowrap') + tr.trExpand = function(changeState){ + if(this.trChildren.length < 1) return + if(changeState) { + this.trChildrenVisible = true + $('#state', this).get(0).src= imgCollapse + } + var doit = changeState || this.trChildrenVisible + $.each(this.trChildren, function(i, ctr) { + if(doit) $(ctr).css('display', 'table-row') + ctr.trExpand() + }) + } + tr.trCollapse = function(changeState){ + if(this.trChildren.length < 1) return + if(changeState) { + this.trChildrenVisible = false + $('#state', this).get(0).src= imgExpand + } + $.each(this.trChildren, function(i, ctr) { + $(ctr).css('display', 'none') + ctr.trCollapse() + }) + } + $(tr).click(function() { + this.trChildrenVisible ? this.trCollapse(true) : this.trExpand(true) + }) + }) + return table + } + + function appendTreetable(tree, options) { + function inALine(nodes) { + var tr = $('') + $.each(nodes, function(i, node){ + tr.append($('
' + desc + '' + idAttr + '' + key + '
' + node[attr] + '' + node[idAttr] + '' + value + '
').append(node)) + }) + return $('').append(tr) + + } + options = options || {} + options.idAttr = (options.idAttr || 'id') + options.childrenAttr = (options.childrenAttr || 'children') + var controls = (options.controls || []) + + if (!options.mountPoint) + options.mountPoint = $('body') + + if (options.depthFirst) + depthFirst(tree, options.depthFirst, options.childrenAttr) + var rendered = renderTree(tree, options.childrenAttr, options.idAttr, + options.renderedAttr, options.renderer, options.tableAttributes) + + treeTable(rendered) + if (options.replaceContent) { + options.mountPoint.html('') + } + var initialExpandLevel = options.initialExpandLevel ? parseInt(options.initialExpandLevel) : -1 + initialExpandLevel = Math.min(initialExpandLevel, tree[0].maxLevel) + rendered.expandLevel(initialExpandLevel) + if(options.slider){ + var slider = $('
') + slider.width('200px') + slider.slider({ + min : 1, + max : tree[0].maxLevel, + range : "min", + value : initialExpandLevel, + slide : function(event, ui) { + rendered.expandLevel(ui.value) + } + }) + controls = [slider].concat(options.controls) + } + + if(controls.length >0){ + options.mountPoint.append(inALine(controls)) + } + options.mountPoint.append(rendered) + return rendered + } + + return { + depthFirst : depthFirst, + makeTree : makeTree, + renderTree : renderTree, + attr2attr : attr2attr, + treeTable : treeTable, + appendTreetable : appendTreetable, + jsTreeTable : '1.0', + register : function(target){ + $.each(this, function(key, value){ if(key != 'register') target[key] = value}) + } + } +})(); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/appservice.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/appservice.js new file mode 100644 index 0000000..4770583 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/appservice.js @@ -0,0 +1,12 @@ + +var app = angular.module('sentinelDashboardApp'); + +app.service('AppService', ['$http', function ($http) { + this.getApps = function () { + return $http({ + // url: 'app/mock_infos', + url: 'app/briefinfos.json', + method: 'GET' + }); + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/auth_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/auth_service.js new file mode 100644 index 0000000..fec1cf4 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/auth_service.js @@ -0,0 +1,25 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('AuthService', ['$http', function ($http) { + this.check = function () { + return $http({ + url: '/auth/check', + method: 'POST' + }); + }; + + this.login = function (param) { + return $http({ + url: '/auth/login', + params: param, + method: 'POST' + }); + }; + + this.logout = function () { + return $http({ + url: '/auth/logout', + method: 'POST' + }); + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/authority_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/authority_service.js new file mode 100644 index 0000000..42a6101 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/authority_service.js @@ -0,0 +1,56 @@ +/** + * Authority rule service. + */ +angular.module('sentinelDashboardApp').service('AuthorityRuleService', ['$http', function ($http) { + this.queryMachineRules = function(app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: '/authority/rules', + params: param, + method: 'GET' + }); + }; + + this.addNewRule = function(rule) { + return $http({ + url: '/authority/rule', + data: rule, + method: 'POST' + }); + }; + + this.saveRule = function (entity) { + return $http({ + url: '/authority/rule/' + entity.id, + data: entity, + method: 'PUT' + }); + }; + + this.deleteRule = function (entity) { + return $http({ + url: '/authority/rule/' + entity.id, + method: 'DELETE' + }); + }; + + this.checkRuleValid = function checkRuleValid(rule) { + if (rule.resource === undefined || rule.resource === '') { + alert('资源名称不能为空'); + return false; + } + if (rule.limitApp === undefined || rule.limitApp === '') { + alert('流控针对应用不能为空'); + return false; + } + if (rule.strategy === undefined) { + alert('必须选择黑白名单模式'); + return false; + } + return true; + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/cluster_state_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/cluster_state_service.js new file mode 100644 index 0000000..7bca816 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/cluster_state_service.js @@ -0,0 +1,73 @@ +/** + * Cluster state control service. + * + * @author Eric Zhao + */ +angular.module('sentinelDashboardApp').service('ClusterStateService', ['$http', function ($http) { + + this.fetchClusterUniversalStateSingle = function(app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: '/cluster/state_single', + params: param, + method: 'GET' + }); + }; + + this.fetchClusterUniversalStateOfApp = function(app) { + return $http({ + url: '/cluster/state/' + app, + method: 'GET' + }); + }; + + this.fetchClusterServerStateOfApp = function(app) { + return $http({ + url: '/cluster/server_state/' + app, + method: 'GET' + }); + }; + + this.fetchClusterClientStateOfApp = function(app) { + return $http({ + url: '/cluster/client_state/' + app, + method: 'GET' + }); + }; + + this.modifyClusterConfig = function(config) { + return $http({ + url: '/cluster/config/modify_single', + data: config, + method: 'POST' + }); + }; + + this.applyClusterFullAssignOfApp = function(app, clusterMap) { + return $http({ + url: '/cluster/assign/all_server/' + app, + data: clusterMap, + method: 'POST' + }); + }; + + this.applyClusterSingleServerAssignOfApp = function(app, request) { + return $http({ + url: '/cluster/assign/single_server/' + app, + data: request, + method: 'POST' + }); + }; + + this.applyClusterServerBatchUnbind = function(app, machineSet) { + return $http({ + url: '/cluster/assign/unbind_server/' + app, + data: machineSet, + method: 'POST' + }); + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/degrade_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/degrade_service.js new file mode 100644 index 0000000..a242b22 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/degrade_service.js @@ -0,0 +1,97 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('DegradeService', ['$http', function ($http) { + this.queryMachineRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: 'degrade/rules.json', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + return $http({ + url: '/degrade/rule', + data: rule, + method: 'POST' + }); + }; + + this.saveRule = function (rule) { + var param = { + id: rule.id, + resource: rule.resource, + limitApp: rule.limitApp, + grade: rule.grade, + count: rule.count, + timeWindow: rule.timeWindow, + statIntervalMs: rule.statIntervalMs, + minRequestAmount: rule.minRequestAmount, + slowRatioThreshold: rule.slowRatioThreshold, + }; + return $http({ + url: '/degrade/rule/' + rule.id, + data: param, + method: 'PUT' + }); + }; + + this.deleteRule = function (rule) { + return $http({ + url: '/degrade/rule/' + rule.id, + method: 'DELETE' + }); + }; + + this.checkRuleValid = function (rule) { + if (rule.resource === undefined || rule.resource === '') { + alert('资源名称不能为空'); + return false; + } + if (rule.grade === undefined || rule.grade < 0) { + alert('未知的降级策略'); + return false; + } + if (rule.count === undefined || rule.count === '' || rule.count < 0) { + alert('降级阈值不能为空或小于 0'); + return false; + } + if (rule.timeWindow == undefined || rule.timeWindow === '' || rule.timeWindow <= 0) { + alert('熔断时长必须大于 0s'); + return false; + } + if (rule.minRequestAmount == undefined || rule.minRequestAmount <= 0) { + alert('最小请求数目需大于 0'); + return false; + } + if (rule.statIntervalMs == undefined || rule.statIntervalMs <= 0) { + alert('统计窗口时长需大于 0s'); + return false; + } + if (rule.statIntervalMs !== undefined && rule.statIntervalMs > 60 * 1000 * 2) { + alert('统计窗口时长不能超过 120 分钟'); + return false; + } + // 异常比率类型. + if (rule.grade == 1 && rule.count > 1) { + alert('异常比率超出范围:[0.0 - 1.0]'); + return false; + } + if (rule.grade == 0) { + if (rule.slowRatioThreshold == undefined) { + alert('慢调用比率不能为空'); + return false; + } + if (rule.slowRatioThreshold < 0 || rule.slowRatioThreshold > 1) { + alert('慢调用比率超出范围:[0.0 - 1.0]'); + return false; + } + } + return true; + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v1.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v1.js new file mode 100644 index 0000000..051a3c7 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v1.js @@ -0,0 +1,119 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('FlowServiceV1', ['$http', function ($http) { + this.queryMachineRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: '/v1/flow/rules', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + var param = { + resource: rule.resource, + limitApp: rule.limitApp, + grade: rule.grade, + count: rule.count, + strategy: rule.strategy, + refResource: rule.refResource, + controlBehavior: rule.controlBehavior, + warmUpPeriodSec: rule.warmUpPeriodSec, + maxQueueingTimeMs: rule.maxQueueingTimeMs, + app: rule.app, + ip: rule.ip, + port: rule.port + }; + + return $http({ + url: '/v1/flow/rule', + data: rule, + method: 'POST' + }); + }; + + this.saveRule = function (rule) { + var param = { + id: rule.id, + resource: rule.resource, + limitApp: rule.limitApp, + grade: rule.grade, + count: rule.count, + strategy: rule.strategy, + refResource: rule.refResource, + controlBehavior: rule.controlBehavior, + warmUpPeriodSec: rule.warmUpPeriodSec, + maxQueueingTimeMs: rule.maxQueueingTimeMs, + }; + + return $http({ + url: '/v1/flow/save.json', + params: param, + method: 'PUT' + }); + }; + + this.deleteRule = function (rule) { + var param = { + id: rule.id, + app: rule.app + }; + + return $http({ + url: '/v1/flow/delete.json', + params: param, + method: 'DELETE' + }); + }; + + function notNumberAtLeastZero(num) { + return num === undefined || num === '' || isNaN(num) || num < 0; + } + + function notNumberGreaterThanZero(num) { + return num === undefined || num === '' || isNaN(num) || num <= 0; + } + + this.checkRuleValid = function (rule) { + if (rule.resource === undefined || rule.resource === '') { + alert('资源名称不能为空'); + return false; + } + if (rule.count === undefined || rule.count < 0) { + alert('限流阈值必须大于等于 0'); + return false; + } + if (rule.strategy === undefined || rule.strategy < 0) { + alert('无效的流控模式'); + return false; + } + if (rule.strategy == 1 || rule.strategy == 2) { + if (rule.refResource === undefined || rule.refResource == '') { + alert('请填写关联资源或入口'); + return false; + } + } + if (rule.controlBehavior === undefined || rule.controlBehavior < 0) { + alert('无效的流控整形方式'); + return false; + } + if (rule.controlBehavior == 1 && notNumberGreaterThanZero(rule.warmUpPeriodSec)) { + alert('预热时长必须大于 0'); + return false; + } + if (rule.controlBehavior == 2 && notNumberGreaterThanZero(rule.maxQueueingTimeMs)) { + alert('排队超时时间必须大于 0'); + return false; + } + if (rule.clusterMode && (rule.clusterConfig === undefined || rule.clusterConfig.thresholdType === undefined)) { + alert('集群限流配置不正确'); + return false; + } + return true; + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v2.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v2.js new file mode 100644 index 0000000..716d66d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/flow_service_v2.js @@ -0,0 +1,85 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('FlowServiceV2', ['$http', function ($http) { + this.queryMachineRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: '/v2/flow/rules', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + return $http({ + url: '/v2/flow/rule', + data: rule, + method: 'POST' + }); + }; + + this.saveRule = function (rule) { + return $http({ + url: '/v2/flow/rule/' + rule.id, + data: rule, + method: 'PUT' + }); + }; + + this.deleteRule = function (rule) { + return $http({ + url: '/v2/flow/rule/' + rule.id, + method: 'DELETE' + }); + }; + + function notNumberAtLeastZero(num) { + return num === undefined || num === '' || isNaN(num) || num < 0; + } + + function notNumberGreaterThanZero(num) { + return num === undefined || num === '' || isNaN(num) || num <= 0; + } + + this.checkRuleValid = function (rule) { + if (rule.resource === undefined || rule.resource === '') { + alert('资源名称不能为空'); + return false; + } + if (rule.count === undefined || rule.count < 0) { + alert('限流阈值必须大于等于 0'); + return false; + } + if (rule.strategy === undefined || rule.strategy < 0) { + alert('无效的流控模式'); + return false; + } + if (rule.strategy == 1 || rule.strategy == 2) { + if (rule.refResource === undefined || rule.refResource == '') { + alert('请填写关联资源或入口'); + return false; + } + } + if (rule.controlBehavior === undefined || rule.controlBehavior < 0) { + alert('无效的流控整形方式'); + return false; + } + if (rule.controlBehavior == 1 && notNumberGreaterThanZero(rule.warmUpPeriodSec)) { + alert('预热时长必须大于 0'); + return false; + } + if (rule.controlBehavior == 2 && notNumberGreaterThanZero(rule.maxQueueingTimeMs)) { + alert('排队超时时间必须大于 0'); + return false; + } + if (rule.clusterMode && (rule.clusterConfig === undefined || rule.clusterConfig.thresholdType === undefined)) { + alert('集群限流配置不正确'); + return false; + } + return true; + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/api_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/api_service.js new file mode 100644 index 0000000..373f71d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/api_service.js @@ -0,0 +1,73 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('GatewayApiService', ['$http', function ($http) { + this.queryApis = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: '/gateway/api/list.json', + params: param, + method: 'GET' + }); + }; + + this.newApi = function (api) { + return $http({ + url: '/gateway/api/new.json', + data: api, + method: 'POST' + }); + }; + + this.saveApi = function (api) { + return $http({ + url: '/gateway/api/save.json', + data: api, + method: 'POST' + }); + }; + + this.deleteApi = function (api) { + var param = { + id: api.id, + app: api.app + }; + return $http({ + url: '/gateway/api/delete.json', + params: param, + method: 'POST' + }); + }; + + this.checkApiValid = function (api, apiNames) { + if (api.apiName === undefined || api.apiName === '') { + alert('API名称不能为空'); + return false; + } + + if (api.predicateItems == null || api.predicateItems.length === 0) { + // Should never happen since no remove button will display when only one predicateItem. + alert('至少有一个匹配规则'); + return false; + } + + for (var i = 0; i < api.predicateItems.length; i++) { + var predicateItem = api.predicateItems[i]; + var pattern = predicateItem.pattern; + if (pattern === undefined || pattern === '') { + alert('匹配串不能为空,请检查'); + return false; + } + } + + if (apiNames.indexOf(api.apiName) !== -1) { + alert('API名称(' + api.apiName + ')已存在'); + return false; + } + + return true; + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/flow_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/flow_service.js new file mode 100644 index 0000000..b026b32 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/gateway/flow_service.js @@ -0,0 +1,76 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('GatewayFlowService', ['$http', function ($http) { + this.queryRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + + return $http({ + url: '/gateway/flow/list.json', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + return $http({ + url: '/gateway/flow/new.json', + data: rule, + method: 'POST' + }); + }; + + this.saveRule = function (rule) { + return $http({ + url: '/gateway/flow/save.json', + data: rule, + method: 'POST' + }); + }; + + this.deleteRule = function (rule) { + var param = { + id: rule.id, + app: rule.app + }; + + return $http({ + url: '/gateway/flow/delete.json', + params: param, + method: 'POST' + }); + }; + + this.checkRuleValid = function (rule) { + if (rule.resource === undefined || rule.resource === '') { + alert('API名称不能为空'); + return false; + } + + if (rule.paramItem != null) { + if (rule.paramItem.parseStrategy == 2 || + rule.paramItem.parseStrategy == 3 || + rule.paramItem.parseStrategy == 4) { + if (rule.paramItem.fieldName === undefined || rule.paramItem.fieldName === '') { + alert('当参数属性为Header、URL参数、Cookie时,参数名称不能为空'); + return false; + } + + if (rule.paramItem.pattern === '') { + alert('匹配串不能为空'); + return false; + } + } + } + + if (rule.count === undefined || rule.count < 0) { + alert((rule.grade === 1 ? 'QPS阈值' : '线程数') + '必须大于等于 0'); + return false; + } + + return true; + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/identityservice.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/identityservice.js new file mode 100644 index 0000000..926c002 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/identityservice.js @@ -0,0 +1,30 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('IdentityService', ['$http', function ($http) { + + this.fetchIdentityOfMachine = function (ip, port, searchKey) { + var param = { + ip: ip, + port: port, + searchKey: searchKey + }; + return $http({ + url: 'resource/machineResource.json', + params: param, + method: 'GET' + }); + }; + this.fetchClusterNodeOfMachine = function (ip, port, searchKey) { + var param = { + ip: ip, + port: port, + type: 'cluster', + searchKey: searchKey + }; + return $http({ + url: 'resource/machineResource.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/machineservice.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/machineservice.js new file mode 100644 index 0000000..2d3b5e8 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/machineservice.js @@ -0,0 +1,25 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('MachineService', ['$http', '$httpParamSerializerJQLike', + function ($http, $httpParamSerializerJQLike) { + this.getAppMachines = function (app) { + return $http({ + url: 'app/' + app + '/machines.json', + method: 'GET' + }); + }; + this.removeAppMachine = function (app, ip, port) { + return $http({ + url: 'app/' + app + '/machine/remove.json', + method: 'POST', + headers: { + 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data: $httpParamSerializerJQLike({ + ip: ip, + port: port + }) + }); + }; + }] +); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/metricservice.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/metricservice.js new file mode 100644 index 0000000..8d8a38e --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/metricservice.js @@ -0,0 +1,36 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('MetricService', ['$http', function ($http) { + + this.queryAppSortedIdentities = function (params) { + return $http({ + url: '/metric/queryTopResourceMetric.json', + params: params, + method: 'GET' + }); + }; + + this.queryByAppAndIdentity = function (params) { + return $http({ + url: '/metric/queryByAppAndResource.json', + params: params, + method: 'GET' + }); + }; + + this.queryByMachineAndIdentity = function (ip, port, identity, startTime, endTime) { + var param = { + ip: ip, + port: port, + identity: identity, + startTime: startTime.getTime(), + endTime: endTime.getTime() + }; + + return $http({ + url: '/metric/queryByAppAndResource.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/param_flow_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/param_flow_service.js new file mode 100644 index 0000000..2e23555 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/param_flow_service.js @@ -0,0 +1,104 @@ +/** + * Parameter flow control service. + * + * @author Eric Zhao + */ +angular.module('sentinelDashboardApp').service('ParamFlowService', ['$http', function ($http) { + this.queryMachineRules = function(app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: '/paramFlow/rules', + params: param, + method: 'GET' + }); + }; + + this.addNewRule = function(rule) { + return $http({ + url: '/paramFlow/rule', + data: rule, + method: 'POST' + }); + }; + + this.saveRule = function (entity) { + return $http({ + url: '/paramFlow/rule/' + entity.id, + data: entity, + method: 'PUT' + }); + }; + + this.deleteRule = function (entity) { + return $http({ + url: '/paramFlow/rule/' + entity.id, + method: 'DELETE' + }); + }; + + function isNumberClass(classType) { + return classType === 'int' || classType === 'double' || + classType === 'float' || classType === 'long' || classType === 'short'; + } + + function isByteClass(classType) { + return classType === 'byte'; + } + + function notNumberAtLeastZero(num) { + return num === undefined || num === '' || isNaN(num) || num < 0; + } + + function notGoodNumber(num) { + return num === undefined || num === '' || isNaN(num); + } + + function notGoodNumberBetweenExclusive(num, l ,r) { + return num === undefined || num === '' || isNaN(num) || num < l || num > r; + } + + function notValidParamItem(curExItem) { + if (isNumberClass(curExItem.classType) && notGoodNumber(curExItem.object)) { + return true; + } + if (isByteClass(curExItem.classType) && notGoodNumberBetweenExclusive(curExItem.object, -128, 127)) { + return true; + } + return curExItem.object === undefined || curExItem.classType === undefined || + notNumberAtLeastZero(curExItem.count); + } + + this.checkRuleValid = function (rule) { + if (!rule.resource || rule.resource === '') { + alert('资源名称不能为空'); + return false; + } + if (rule.grade != 1) { + alert('未知的限流模式'); + return false; + } + if (rule.count < 0) { + alert('限流阈值必须大于等于 0'); + return false; + } + if (rule.paramIdx === undefined || rule.paramIdx === '' || isNaN(rule.paramIdx) || rule.paramIdx < 0) { + alert('热点参数索引必须大于等于 0'); + return false; + } + if (rule.paramFlowItemList !== undefined) { + for (var i = 0; i < rule.paramFlowItemList.length; i++) { + var item = rule.paramFlowItemList[i]; + if (notValidParamItem(item)) { + alert('热点参数例外项不合法,请检查值和类型是否正确:参数为 ' + item.object + ', 类型为 ' + + item.classType + ', 限流阈值为 ' + item.count); + return false; + } + } + } + return true; + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/systemservice.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/systemservice.js new file mode 100644 index 0000000..8b47679 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/systemservice.js @@ -0,0 +1,77 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('SystemService', ['$http', function ($http) { + this.queryMachineRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: 'system/rules.json', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + var param = { + app: rule.app, + ip: rule.ip, + port: rule.port + }; + if (rule.grade == 0) {// avgLoad + param.highestSystemLoad = rule.highestSystemLoad; + } else if (rule.grade == 1) {// avgRt + param.avgRt = rule.avgRt; + } else if (rule.grade == 2) {// maxThread + param.maxThread = rule.maxThread; + } else if (rule.grade == 3) {// qps + param.qps = rule.qps; + } else if (rule.grade == 4) {// cpu + param.highestCpuUsage = rule.highestCpuUsage; + } + + return $http({ + url: '/system/new.json', + params: param, + method: 'GET' + }); + }; + + this.saveRule = function (rule) { + var param = { + id: rule.id, + }; + if (rule.grade == 0) {// avgLoad + param.highestSystemLoad = rule.highestSystemLoad; + } else if (rule.grade == 1) {// avgRt + param.avgRt = rule.avgRt; + } else if (rule.grade == 2) {// maxThread + param.maxThread = rule.maxThread; + } else if (rule.grade == 3) {// qps + param.qps = rule.qps; + } else if (rule.grade == 4) {// cpu + param.highestCpuUsage = rule.highestCpuUsage; + } + + return $http({ + url: '/system/save.json', + params: param, + method: 'GET' + }); + }; + + this.deleteRule = function (rule) { + var param = { + id: rule.id, + app: rule.app + }; + + return $http({ + url: '/system/delete.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/version_service.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/version_service.js new file mode 100644 index 0000000..1322f56 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/scripts/services/version_service.js @@ -0,0 +1,10 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('VersionService', ['$http', function ($http) { + this.version = function () { + return $http({ + url: '/version', + method: 'GET' + }); + }; +}]); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/main.css b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/main.css new file mode 100644 index 0000000..bb1db2b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/main.css @@ -0,0 +1,1756 @@ +.browsehappy { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +body { + padding: 0; +} + +/* Everything but the jumbotron gets side spacing for mobile first views */ + +.header, +.marketing, +.footer { + padding-left: 15px; + padding-right: 15px; +} + + +/* Custom page header */ + +.header { + border-bottom: 1px solid #e5e5e5; + margin-bottom: 10px; +} + + +/* Make the masthead heading the same height as the navigation */ + +.header h3 { + margin-top: 0; + margin-bottom: 0; + line-height: 40px; + padding-bottom: 19px; +} + + +/* Custom page footer */ + +.footer { + padding-top: 19px; + color: #777; + border-top: 1px solid #e5e5e5; +} + +.container-narrow > hr { + margin: 30px 0; +} + + +/* Main marketing message and sign up button */ + +.jumbotron { + text-align: center; + border-bottom: 1px solid #e5e5e5; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + + +/* Supporting marketing content */ + +.marketing { + margin: 40px 0; +} + +.marketing p + h4 { + margin-top: 28px; +} + + +/* Responsive: Portrait tablets and up */ + +@media screen and (min-width: 768px) { + .container { + width: inherit; + margin-left: 60px; + margin-right: 5px; + } + /* Remove the padding we set earlier */ + .header, + .marketing, + .footer { + padding-left: 0; + padding-right: 0; + } + /* Space out the masthead */ + .header { + margin-bottom: 30px; + } + /* Remove the bottom border on the jumbotron for visual effect */ + .jumbotron { + border-bottom: 0; + } +} + +.navbar-inverse { + background-color: #1d9d74; + border-color: #1b926c; +} + +.navbar-inverse .navbar-nav > li > a { + color: #b0ddce; + font-size: 15px; +} + +.navbar-inverse .navbar-nav>.open>a, +.navbar-inverse .navbar-nav>.open>a:focus, +.navbar-inverse .navbar-nav>.open>a:hover { + background-color: #1b926c; +} + +@media (min-width: 900px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: 0%; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} + +.dropdown-menu { + min-width: 100px !important; +} + +.nav-sidebar li.active a { + background: #DDD; +} + +.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus { + background: #1d9d74; + /*background: #d9d9d9;*/ + color: white; +} + +.broadcast-message, +.broadcast-message-preview { + padding: 10px; + text-align: center; + background: #555; + color: #BBB; + margin-top: 50px; +} + +.card { + position: relative; + border: 1px solid #d9d9d9; + border-radius: 3px; + color: #666; + background-color: #fff; + width: 100%; + border-radius: 5px; +} + +.card .card-header { + padding: 9px 0; + height: 40px; + background: #555; + color: #fff; + text-align: center; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.card .card-body { + padding: 12px 10px; +} + +.card .card-footer { + height: 20px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .detail-brand { + float: left; + width: 30%; + line-height: 98px; + font-size: 30px; + text-align: center; + color: white; +} + +.card .default { + background: #1d9d74; +} + +.card .info { + background: #6EBEE7; +} + +.card .warn { + background: #ED7F54; +} + +.card .danger { + background: #6583BE; +} + +.card .detail .text-default { + color: #1d9d74; +} + +.card .detail .text-info { + color: #6EBEE7; +} + +.card .detail .text-warn { + color: #ED7F54; +} + +.card .detail .text-danger { + color: #6583BE; +} + +.card .detail { + float: right; + width: 70%; + line-height: 98px; + text-align: center; +} + +.card .detail .text { + font-size: 12px; +} + +.card .detail .number { + font-size: 30px; + font-weight: 500; +} + +.h100 { + height: 100px; +} + +.inline { + display: inline; +} + +.separator { + height: 1px; + background-color: #e5e5e5; + margin-top: 10px; +} + +.card > .card-body > table > thead > tr > td, +.card > .card-body > table > tbody > tr > td { + word-wrap: break-word; + word-break: break-all; +} + +.card > .card-body > table > thead > tr > td { + font-weight: 500; + font-size: 13px; + text-align: center; +} + +.card > .card-body > table > thead > tr > td > span { + font-weight: 500; + font-size: 10px; +} + +.card > .card-body > table > tbody > tr > td { + font-size: 12px; + text-align: center; +} + +.card > .card-body > table > tbody > tr > td > a { + color: #666; +} + +.thumbnails > .card > .card-body > table > thead > tr > td, +.thumbnails > .card > .card-body > table > tbody > tr > td { + font-size: 12px; + color: #777; + word-wrap: break-word; + word-break: break-all; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(n+2) { + text-align: center; +} + +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(n+2) { + font-weight: 700; + text-align: center; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(1), +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(1) { + text-align: left; +} + +.tools-header { + background: whitesmoke; + padding: 9px 0; + height: 40px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.tools-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.tools-header .brand > a { + color: #666; +} + +.tools-header > button, +.tools-header > select, +.tools-header > a { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 25px; + padding: 0 10px; + line-height: 25px; + color: #666; +} + +.tools-header .paged { + margin-right: 0px; +} + +.btn { + height: 32px; +} + +.btn.btn-main { + color: #ffffff; + background-color: #337ab7; + border-color: #337ab7; +} + +.btn:focus, +.btn:active { + outline: none !important; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + + +.btn.btn-danger-tag { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; + line-height: 1px; + font-size: 11px; + padding: 4px 4px; +} + +.btn.btn-danger { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn.btn-danger:hover, +.btn.btn-danger:focus, +.btn.btn-danger:active { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.form-control { + height : 32px; +} + +.form-control:focus { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + +.form-control { + border-radius: 8px; +} + +.input-label:before { + display: inline-block; + content: "*"; + color: #f44336; + font-family: SimSun; + font-size: 12px; + -webkit-transform: TranslateX(-10px); + -ms-transform: TranslateX(-10px); + transform: TranslateX(-10px); +} + +.label.label-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.badge-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.bootstrap-tagsinput { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + color: #555; + vertical-align: middle; + border-radius: 4px; + /* max-width: 100%; */ + width: 85%; + height: 100px; + line-height: 20px; + cursor: text; +} + +.bootstrap-tagsinput > .dropdown-menu { + min-width: 40px; + font-size: 12px; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + background-color: #1d9d74; + background-image: -webkit-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -o-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#1d9d74), to(#1d9d74)); + background-image: linear-gradient(to bottom, #1d9d74 0, #1d9d74 100%); + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#1d9d74', endColorstr='#1d9d74', GradientType=0); + background-repeat: repeat-x; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + color: #fff; + text-decoration: none; + background-color: #1d9d74; + outline: 0; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus { + color: white; + text-decoration: none; + outline: 0; + background-color: #1d9d74; +} + +.inputs-header { + padding: 9px 0; + height: 50px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.inputs-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.inputs-header .brand > a { + color: #666; +} + +.inputs-header > input { + float: right; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; +} + +.inputs-header > a { + float: right; + margin: 1px 10px; + height: 30px; + padding: 5 5px; +} + +.inputs-header > select { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; + height: 25px; + font-size: 12px; +} + +.witdh-150 { + max-width: 150px; +} + +.witdh-200 { + max-width: 200px; +} + +.width-200 { + max-width: 200px; +} + +.witdh-300 { + max-width: 300px; +} + +.width-300 { + max-width: 300px; +} + +.card.highlight { + border-color: #d9534f; +} + +.card .pagination-footer { + height: 40px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .pagination-footer .tools { + font-size: 12px; + margin: 11px 0; + float: right; + display: inline; + margin-right: 20px; +} + +.card > .pagination-footer > .tools > span > input { + height: 25px; + max-width: 50px; + display: inline; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 8px 0; + float: right; + border-radius: 4px; +} + + +.pagination > a { + margin-right: 5px; + height: 28px; + width: 28px; + padding: 5px 0px; +} + +.pagination > .btn.active { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + + + + +.datepicker > .table > thead > tr > td, .datepicker > .table > tbody > tr > td, +.timepicker > .table > thead > tr > td, .timepicker > .table > tbody > tr > td { + padding: 5px 3px; +} + +.datepicker > .table > thead > tr > td > .btn, .datepicker > .table > tbody > tr > td > .btn, +.timepicker > .table > thead > tr > td > .btn, .timepicker > .table > tbody > tr > td > .btn { + border: 1px solid #FFFDFD; +} + +.datepicker > .table > thead > tr > td > .btn-default:hover, +.datepicker > .table > thead > tr > td > .btn-default:focus, +.datepicker > .table > thead > tr > td > .btn-default:active, +.datepicker > .table > tbody > tr > td > .btn-default:hover, +.datepicker > .table > tbody > tr > td > .btn-default:focus, +.datepicker > .table > tbody > tr > td > .btn-default:active, +.timepicker > .table > thead > tr > td > .btn-default:hover, +.timepicker > .table > thead > tr > td > .btn-default:focus, +.timepicker > .table > thead > tr > td > .btn-default:active, +.timepicker > .table > tbody > tr > td > .btn-default:hover, +.timepicker > .table > tbody > tr > td > .btn-default:focus, +.timepicker > .table > tbody > tr > td > .btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.datepicker > .table > thead > tr > td > a, .datepicker > .table > tbody > tr > td > a, +.timepicker > .table > thead > tr > td > a, .timepicker > .table > tbody > tr > td > a { + height: 25px; + width: 25px; + padding: 3px 0px; +} + +.datepicker > .table > tbody > tr:first-child > td > a { + padding: 4px 0px; +} + +.datepicker > .table > thead > tr > td > a.btn.active, +.datepicker > .table > tbody > tr > td > a.btn.active, +.timepicker > .table > thead > tr > td > a.btn.active, +.timepicker > .table > tbody > tr > td > a.btn.active { +/* color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74;*/ + color: #1d9d74; + border-color: #1d9d74; + background: white; + box-shadow: inset 0 0px 0px rgba(0,0,0,0.125); +} + +.datepicker > .table > thead > tr > td:not(:first-child):last-child > a, +.timepicker > .table > thead > tr > td:not(:first-child):last-child > a { + height: 25px; + width: 50px; + padding: 5px 0px; +} + +.datepicker > .table > tbody > tr > td > a, +.timepicker > .table > tbody > tr > td > a { + margin-left: 8px; +} + + +.selectize-input-200 > .selectize-input { + min-width: 250px; +} + +.highlight-border { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +}.browsehappy { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +body { + padding: 0; +} + + +/* Everything but the jumbotron gets side spacing for mobile first views */ + +.header, +.marketing, +.footer { + padding-left: 15px; + padding-right: 15px; +} + + +/* Custom page header */ + +.header { + border-bottom: 1px solid #e5e5e5; + margin-bottom: 10px; +} + + +/* Make the masthead heading the same height as the navigation */ + +.header h3 { + margin-top: 0; + margin-bottom: 0; + line-height: 40px; + padding-bottom: 19px; +} + + +/* Custom page footer */ + +.footer { + padding-top: 19px; + color: #777; + border-top: 1px solid #e5e5e5; +} + +.container-narrow > hr { + margin: 30px 0; +} + + +/* Main marketing message and sign up button */ + +.jumbotron { + text-align: center; + border-bottom: 1px solid #e5e5e5; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + + +/* Supporting marketing content */ + +.marketing { + margin: 40px 0; +} + +.marketing p + h4 { + margin-top: 28px; +} + + +/* Responsive: Portrait tablets and up */ + +@media screen and (min-width: 768px) { + .container { + width: inherit; + margin-left: 60px; + margin-right: 5px; + } + /* Remove the padding we set earlier */ + .header, + .marketing, + .footer { + padding-left: 0; + padding-right: 0; + } + /* Space out the masthead */ + .header { + margin-bottom: 30px; + } + /* Remove the bottom border on the jumbotron for visual effect */ + .jumbotron { + border-bottom: 0; + } +} + +.navbar-inverse { + background-color: #1d9d74; + border-color: #1b926c; +} + +.navbar-inverse .navbar-nav > li > a { + color: #b0ddce; + font-size: 15px; +} + +.navbar-inverse .navbar-nav>.open>a, +.navbar-inverse .navbar-nav>.open>a:focus, +.navbar-inverse .navbar-nav>.open>a:hover { + background-color: #1b926c; +} + +@media (min-width: 900px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: 0%; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} + +.dropdown-menu { + min-width: 100px !important; +} + +.nav-sidebar li.active a { + background: #DDD; +} + +.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus { + background: #1d9d74; + /*background: #d9d9d9;*/ + color: white; +} + +.broadcast-message, +.broadcast-message-preview { + padding: 10px; + text-align: center; + background: #555; + color: #BBB; + margin-top: 50px; +} + +.card { + position: relative; + border: 1px solid #d9d9d9; + border-radius: 3px; + color: #666; + background-color: #fff; + width: 100%; + border-radius: 5px; +} + +.card .card-header { + padding: 9px 0; + height: 40px; + background: #555; + color: #fff; + text-align: center; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.card .card-body { + padding: 12px 10px; +} + +.card .card-footer { + height: 20px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .detail-brand { + float: left; + width: 30%; + line-height: 98px; + font-size: 30px; + text-align: center; + color: white; +} + +.card .default { + background: #1d9d74; +} + +.card .info { + background: #6EBEE7; +} + +.card .warn { + background: #ED7F54; +} + +.card .danger { + background: #6583BE; +} + +.card .detail .text-default { + color: #1d9d74; +} + +.card .detail .text-info { + color: #6EBEE7; +} + +.card .detail .text-warn { + color: #ED7F54; +} + +.card .detail .text-danger { + color: #6583BE; +} + +.card .detail { + float: right; + width: 70%; + line-height: 98px; + text-align: center; +} + +.card .detail .text { + font-size: 12px; +} + +.card .detail .number { + font-size: 30px; + font-weight: 500; +} + +.h100 { + height: 100px; +} + +.inline { + display: inline; +} + +.separator { + height: 1px; + background-color: #e5e5e5; + margin-top: 10px; +} + +.card > .card-body > table > thead > tr > td, +.card > .card-body > table > tbody > tr > td { + word-wrap: break-word; + word-break: break-all; +} + +.card > .card-body > table > thead > tr > td { + font-weight: 500; + font-size: 13px; + text-align: center; +} + +.card > .card-body > table > thead > tr > td > span { + font-weight: 500; + font-size: 10px; +} + +.card > .card-body > table > tbody > tr > td { + font-size: 12px; + text-align: center; +} + +.card > .card-body > table > tbody > tr > td > a { + color: #666; +} + +.thumbnails > .card > .card-body > table > thead > tr > td, +.thumbnails > .card > .card-body > table > tbody > tr > td { + font-size: 12px; + color: #777; + word-wrap: break-word; + word-break: break-all; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(n+2) { + text-align: center; +} + +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(n+2) { + font-weight: 700; + text-align: center; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(1), +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(1) { + text-align: left; +} + +.tools-header { + background: whitesmoke; + padding: 9px 0; + height: 40px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.tools-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.tools-header .brand > a { + color: #666; +} + +.tools-header > button, +.tools-header > select, +.tools-header > a { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 25px; + padding: 0 10px; + line-height: 25px; + color: #666; +} + +.tools-header .paged { + margin-right: 0px; +} + +.btn.btn-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.btn:focus, +.btn:active { + outline: none !important; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.btn-default-inverse { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.btn-default-inverse:hover, +.btn-default-inverse:focus, +.btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.btn.btn-danger-tag { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; + line-height: 1px; + font-size: 11px; + padding: 4px 4px; +} + +.btn.btn-danger { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn.btn-danger:hover, +.btn.btn-danger:focus, +.btn.btn-danger:active { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.form-control { + height : 32px; +} + +.form-control:focus { + border-color: #1d9d74; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + +.form-control { + border-radius: 8px; +} + +.input-label:before { + display: inline-block; + content: "*"; + color: #f44336; + font-family: SimSun; + font-size: 12px; + -webkit-transform: TranslateX(-10px); + -ms-transform: TranslateX(-10px); + transform: TranslateX(-10px); +} + +.label.label-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.badge-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.bootstrap-tagsinput { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + color: #555; + vertical-align: middle; + border-radius: 4px; + /* max-width: 100%; */ + width: 85%; + height: 100px; + line-height: 20px; + cursor: text; +} + +.bootstrap-tagsinput > .dropdown-menu { + min-width: 40px; + font-size: 12px; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + background-color: #1d9d74; + background-image: -webkit-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -o-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#1d9d74), to(#1d9d74)); + background-image: linear-gradient(to bottom, #1d9d74 0, #1d9d74 100%); + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#1d9d74', endColorstr='#1d9d74', GradientType=0); + background-repeat: repeat-x; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + color: #fff; + text-decoration: none; + background-color: #1d9d74; + outline: 0; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus { + color: white; + text-decoration: none; + outline: 0; + background-color: #1d9d74; +} + +.inputs-header { + padding: 9px 0; + height: 50px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.inputs-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.inputs-header .brand > a { + color: #666; +} + +.inputs-header > input { + float: right; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; +} + +.inputs-header > a { + float: right; + margin: 1px 10px; + height: 30px; + padding: 5 5px; +} + +.inputs-header > select { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; + height: 25px; + font-size: 12px; +} + +.witdh-150 { + max-width: 150px; +} + +.witdh-200 { + max-width: 200px; +} + +.card.highlight { + border-color: #d9534f; +} + +.card .pagination-footer { + height: 40px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .pagination-footer .tools { + font-size: 12px; + margin: 11px 0; + float: right; + display: inline; + margin-right: 20px; +} + +.card > .pagination-footer > .tools > span > input { + height: 25px; + max-width: 50px; + display: inline; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 8px 0; + float: right; + border-radius: 4px; +} + + +.pagination > a { + margin-right: 5px; + height: 28px; + width: 28px; + padding: 5px 0px; +} + +.pagination > .btn.active { + color: #ffffff; + background-color: #449d44; + border-color: #449d44; +} + + + + +.datepicker > .table > thead > tr > td, .datepicker > .table > tbody > tr > td, +.timepicker > .table > thead > tr > td, .timepicker > .table > tbody > tr > td { + padding: 5px 3px; +} + +.datepicker > .table > thead > tr > td > .btn, .datepicker > .table > tbody > tr > td > .btn, +.timepicker > .table > thead > tr > td > .btn, .timepicker > .table > tbody > tr > td > .btn { + border: 1px solid #FFFDFD; +} + +.datepicker > .table > thead > tr > td > .btn-default:hover, +.datepicker > .table > thead > tr > td > .btn-default:focus, +.datepicker > .table > thead > tr > td > .btn-default:active, +.datepicker > .table > tbody > tr > td > .btn-default:hover, +.datepicker > .table > tbody > tr > td > .btn-default:focus, +.datepicker > .table > tbody > tr > td > .btn-default:active, +.timepicker > .table > thead > tr > td > .btn-default:hover, +.timepicker > .table > thead > tr > td > .btn-default:focus, +.timepicker > .table > thead > tr > td > .btn-default:active, +.timepicker > .table > tbody > tr > td > .btn-default:hover, +.timepicker > .table > tbody > tr > td > .btn-default:focus, +.timepicker > .table > tbody > tr > td > .btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.datepicker > .table > thead > tr > td > a, .datepicker > .table > tbody > tr > td > a, +.timepicker > .table > thead > tr > td > a, .timepicker > .table > tbody > tr > td > a { + height: 25px; + width: 25px; + padding: 3px 0px; +} + +.datepicker > .table > tbody > tr:first-child > td > a { + padding: 4px 0px; +} + +.datepicker > .table > thead > tr > td > a.btn.active, +.datepicker > .table > tbody > tr > td > a.btn.active, +.timepicker > .table > thead > tr > td > a.btn.active, +.timepicker > .table > tbody > tr > td > a.btn.active { +/* color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74;*/ + color: #1d9d74; + border-color: #1d9d74; + background: white; + box-shadow: inset 0 0px 0px rgba(0,0,0,0.125); +} + +.datepicker > .table > thead > tr > td:not(:first-child):last-child > a, +.timepicker > .table > thead > tr > td:not(:first-child):last-child > a { + height: 25px; + width: 50px; + padding: 5px 0px; +} + +.datepicker > .table > tbody > tr > td > a, +.timepicker > .table > tbody > tr > td > a { + margin-left: 8px; +} + + +.selectize-input-200 > .selectize-input { + min-width: 250px; +} + +.highlight-border { + border-color: #1d9d74; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + + +.sortorder:after { + content: '\25b2'; +} +.sortorder.reverse:after { + content: '\25bc'; +} + + + +.input-control select { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + position: relative; + border: 1px #d9d9d9 solid; + width: 100%; + height: 100%; + padding: .3125rem; + z-index: 0; +} + +.navbar-inverse { + background-color: #337ab7; + border-color: #337ab7; +} + +.sidebar { + z-index: 1; + width: 220px; + /*position: fixed;*/ + top: 0; + left: 0; + height: 100%; +} + +#page-wrapper { + position: inherit; + margin: 70px 0 0 220px; + padding: 12px 30px; + border-left: 0px solid #e7e7e7; +} + +.sidebar .sidebar-nav.navbar-collapse { + padding-right: 0; + padding-left: 0; + background-color: #F5F5F5; + position: relative; + color: black; + width: 100%; + padding: 0; + margin: 0; + list-style: none inside none; +} + +.sidebar a { + color: #555; +} + +.sidebar ul li:hover { + color:red; +} + +.form-control { + border-radius: 8px; +} + +.form-control:focus { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + +.highlight-border { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +}.browsehappy { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +.btn.btn-main { + color: #ffffff; + background-color: #337ab7; + border-color: #337ab7; +} + +.btn-default-inverse { + color: #337ab7; + border-color: #337ab7; + background: white; +} + +.btn-default-inverse:hover, +.btn-default-inverse:focus, +.btn-default:active { + color: #337ab7; + border-color: #337ab7; + background: white; +} + +.btn-danger-inverse { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.btn-danger-inverse:hover, +.btn-danger-inverse:focus, +.btn-danger:active { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.btn-tab-active, +.btn-tab-active:hover, +.btn-tab-active:focus, +.btn-tab-default:hover, +.btn-tab-default:focus, +.btn-tab-default:active { + color: #337ab7; + border-color: #337ab7; + background: white; + font-weight: 600; +} +.btn-tab-default { + color: #777; + background: white; + font-weight: 600; +} + +.pagination > .btn.active { + color: #ffffff; + background-color: #337ab7; + border-color: #337ab7; +} + +.btn-default:hover, .btn-default:focus, .btn-default:active { + color: #337ab7; + border-color: #337ab7; + background: white; +} + +.bootstrap-switch.bootstrap-switch-on { + border-color: #337ab7; +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success { + color: #fff; + background: #337ab7; +} + +.selectize-input-200 > .selectize-input { + min-width: 200px; + border-color: #337ab7; +} + +.btn-outline-primary { + color: #007bff; + background-color: transparent; + background-image: none; + border-color: #007bff; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:focus, .btn-outline-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #007bff; + background-color: transparent; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, +.show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-secondary { + color: #6c757d; + background-color: transparent; + background-image: none; + border-color: #6c757d; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:focus, .btn-outline-secondary.focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #6c757d; + background-color: transparent; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, +.show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-success { + color: #28a745; + background-color: transparent; + background-image: none; + border-color: #28a745; +} + +.btn-outline-success:hover { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:focus, .btn-outline-success.focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-success.disabled, .btn-outline-success:disabled { + color: #28a745; + background-color: transparent; +} + +.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, +.show > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-info { + color: #17a2b8; + background-color: transparent; + background-image: none; + border-color: #17a2b8; +} + +.btn-outline-info:hover { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:focus, .btn-outline-info.focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-info.disabled, .btn-outline-info:disabled { + color: #17a2b8; + background-color: transparent; +} + +.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, +.show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-warning { + color: #ffc107; + background-color: transparent; + background-image: none; + border-color: #ffc107; +} + +.btn-outline-warning:hover { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:focus, .btn-outline-warning.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #ffc107; + background-color: transparent; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, +.show > .btn-outline-warning.dropdown-toggle { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-danger { + color: #dc3545; + background-color: transparent; + background-image: none; + border-color: #dc3545; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:focus, .btn-outline-danger.focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: #dc3545; + background-color: transparent; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, +.show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-light { + color: #f8f9fa; + background-color: transparent; + background-image: none; + border-color: #f8f9fa; +} + +.btn-outline-light:hover { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:focus, .btn-outline-light.focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-light.disabled, .btn-outline-light:disabled { + color: #f8f9fa; + background-color: transparent; +} + +.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, +.show > .btn-outline-light.dropdown-toggle { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-dark { + color: #343a40; + background-color: transparent; + background-image: none; + border-color: #343a40; +} + +.btn-outline-dark:hover { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:focus, .btn-outline-dark.focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-outline-dark.disabled, .btn-outline-dark:disabled { + color: #343a40; + background-color: transparent; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, +.show > .btn-outline-dark.dropdown-toggle { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/page.css b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/page.css new file mode 100644 index 0000000..af3ba44 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/page.css @@ -0,0 +1,399 @@ +/*! + * Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com) + * Code licensed under the Apache License v2.0. + * For details, see http://www.apache.org/licenses/LICENSE-2.0. + */ + +body { + background-color: #f8f8f8; +} + +.example { + padding: .625rem 1.825rem .625rem 2.5rem; + border: 1px #ccc dashed; + position: relative; + margin: 0 0 .625rem 0; + background-color: #ffffff; +} + +dl dt, +dl dd { + line-height: 1.25rem; +} +dl dt { + font-style: normal; + font-weight: 700; +} +dl dd { + margin-left: .9375rem; +} +dl.horizontal dt { + float: left; + width: 10rem; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} +dl.horizontal dd { + margin-left: 11.25rem; +} + +#wrapper { + width: 100%; +} + +#page-wrapper { + padding: 0 15px; + min-height: 568px; + background-color: #fff; +} + +@media(min-width:768px) { + #page-wrapper { + position: inherit; + margin: 0 0 0 250px; + padding: 0 30px; + border-left: 1px solid #e7e7e7; + } +} + +.navbar-top-links { + margin-right: 0; +} + +.navbar-top-links li { + display: inline-block; +} + +.navbar-top-links li:last-child { + margin-right: 15px; +} + +.navbar-top-links li a { + padding: 15px; + min-height: 50px; +} + +.navbar-top-links .dropdown-menu li { + display: block; +} + +.navbar-top-links .dropdown-menu li:last-child { + margin-right: 0; +} + +.navbar-top-links .dropdown-menu li a { + padding: 3px 20px; + min-height: 0; +} + +.navbar-top-links .dropdown-menu li a div { + white-space: normal; +} + +.navbar-top-links .dropdown-messages, +.navbar-top-links .dropdown-tasks, +.navbar-top-links .dropdown-alerts { + width: 310px; + min-width: 0; +} + +.navbar-top-links .dropdown-messages { + margin-left: 5px; +} + +.navbar-top-links .dropdown-tasks { + margin-left: -59px; +} + +.navbar-top-links .dropdown-alerts { + margin-left: -123px; +} + +.navbar-top-links .dropdown-user { + right: 0; + left: auto; +} + +.sidebar .sidebar-nav.navbar-collapse { + padding-right: 0; + padding-left: 0; + background-color: #71b1d1; + color: #ffffff; + position: relative; + width: 100%; + padding: 0; + margin: 0; + list-style: none inside none; +} + +.sidebar .sidebar-search { + padding: 15px; +} + +.sidebar ul li { + border-bottom: 1px solid #e7e7e7; +} + +.sidebar ul li a.active { + background-color: #ffffff; + color: #ffffff; +} + +.sidebar a{ + color: #fff; +} + +.sidebar .arrow { + float: right; +} + +.sidebar .fa.arrow:before { + content: "\f104"; +} + +.sidebar .active>a>.fa.arrow:before { + content: "\f107"; +} + +.sidebar .nav-second-level li, +.sidebar .nav-third-level li { + border-bottom: 0!important; +} + +.sidebar .nav-second-level li a { + padding-left: 37px; +} + +.sidebar .nav-third-level li a { + padding-left: 52px; +} + +@media(min-width:768px) { + .sidebar { + z-index: 1; + position: absolute; + width: 250px; + margin-top: 51px; + } + + .navbar-top-links .dropdown-messages, + .navbar-top-links .dropdown-tasks, + .navbar-top-links .dropdown-alerts { + margin-left: auto; + } +} + + +.btn-outline { + color: inherit; + background-color: transparent; + transition: all .5s; +} + +.btn-primary.btn-outline { + color: #428bca; +} + +.btn-success.btn-outline { + color: #5cb85c; +} + +.btn-info.btn-outline { + color: #5bc0de; +} + +.btn-warning.btn-outline { + color: #f0ad4e; +} + +.btn-danger.btn-outline { + color: #d9534f; +} + +.btn-primary.btn-outline:hover, +.btn-success.btn-outline:hover, +.btn-info.btn-outline:hover, +.btn-warning.btn-outline:hover, +.btn-danger.btn-outline:hover { + color: #fff; +} + +.chat { + margin: 0; + padding: 0; + list-style: none; +} + +.chat li { + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px dotted #999; +} + +.chat li.left .chat-body { + margin-left: 60px; +} + +.chat li.right .chat-body { + margin-right: 60px; +} + +.chat li .chat-body p { + margin: 0; +} + +.panel .slidedown .glyphicon, +.chat .glyphicon { + margin-right: 5px; +} + +.chat-panel .panel-body { + height: 350px; + overflow-y: scroll; +} + +.login-panel { + margin-top: 25%; +} + +.flot-chart { + display: block; + height: 400px; +} + +.flot-chart-content { + width: 100%; + height: 100%; +} + +.dataTables_wrapper { + position: relative; + clear: both; +} + +table.dataTable thead .sorting, +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting_asc_disabled, +table.dataTable thead .sorting_desc_disabled { + background: 0 0; +} + +table.dataTable thead .sorting_asc:after { + content: "\f0de"; + float: right; + font-family: fontawesome; +} + +table.dataTable thead .sorting_desc:after { + content: "\f0dd"; + float: right; + font-family: fontawesome; +} + +table.dataTable thead .sorting:after { + content: "\f0dc"; + float: right; + font-family: fontawesome; + color: rgba(50,50,50,.5); +} + +.btn-circle { + width: 30px; + height: 30px; + padding: 6px 0; + border-radius: 15px; + text-align: center; + font-size: 12px; + line-height: 1.428571429; +} + +.btn-circle.btn-lg { + width: 50px; + height: 50px; + padding: 10px 16px; + border-radius: 25px; + font-size: 18px; + line-height: 1.33; +} + +.btn-circle.btn-xl { + width: 70px; + height: 70px; + padding: 10px 16px; + border-radius: 35px; + font-size: 24px; + line-height: 1.33; +} + +.show-grid [class^=col-] { + padding-top: 10px; + padding-bottom: 10px; + border: 1px solid #ddd; + background-color: #eee!important; +} + +.show-grid { + margin: 15px 0; +} + +.huge { + font-size: 40px; +} + +.panel-green { + border-color: #5cb85c; +} + +.panel-green .panel-heading { + border-color: #5cb85c; + color: #fff; + background-color: #5cb85c; +} + +.panel-green a { + color: #5cb85c; +} + +.panel-green a:hover { + color: #3d8b3d; +} + +.panel-red { + border-color: #d9534f; +} + +.panel-red .panel-heading { + border-color: #d9534f; + color: #fff; + background-color: #d9534f; +} + +.panel-red a { + color: #d9534f; +} + +.panel-red a:hover { + color: #b52b27; +} + +.panel-yellow { + border-color: #f0ad4e; +} + +.panel-yellow .panel-heading { + border-color: #f0ad4e; + color: #fff; + background-color: #f0ad4e; +} + +.panel-yellow a { + color: #f0ad4e; +} + +.panel-yellow a:hover { + color: #df8a13; +} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/timeline.css b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/timeline.css new file mode 100644 index 0000000..92161eb --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/styles/timeline.css @@ -0,0 +1,180 @@ +.timeline { + position: relative; + padding: 20px 0 20px; + list-style: none; +} + +.timeline:before { + content: " "; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 3px; + margin-left: -1.5px; + background-color: #eeeeee; +} + +.timeline > li { + position: relative; + margin-bottom: 20px; +} + +.timeline > li:before, +.timeline > li:after { + content: " "; + display: table; +} + +.timeline > li:after { + clear: both; +} + +.timeline > li:before, +.timeline > li:after { + content: " "; + display: table; +} + +.timeline > li:after { + clear: both; +} + +.timeline > li > .timeline-panel { + float: left; + position: relative; + width: 46%; + padding: 20px; + border: 1px solid #d4d4d4; + border-radius: 2px; + -webkit-box-shadow: 0 1px 6px rgba(0,0,0,0.175); + box-shadow: 0 1px 6px rgba(0,0,0,0.175); +} + +.timeline > li > .timeline-panel:before { + content: " "; + display: inline-block; + position: absolute; + top: 26px; + right: -15px; + border-top: 15px solid transparent; + border-right: 0 solid #ccc; + border-bottom: 15px solid transparent; + border-left: 15px solid #ccc; +} + +.timeline > li > .timeline-panel:after { + content: " "; + display: inline-block; + position: absolute; + top: 27px; + right: -14px; + border-top: 14px solid transparent; + border-right: 0 solid #fff; + border-bottom: 14px solid transparent; + border-left: 14px solid #fff; +} + +.timeline > li > .timeline-badge { + z-index: 100; + position: absolute; + top: 16px; + left: 50%; + width: 50px; + height: 50px; + margin-left: -25px; + border-radius: 50% 50% 50% 50%; + text-align: center; + font-size: 1.4em; + line-height: 50px; + color: #fff; + background-color: #999999; +} + +.timeline > li.timeline-inverted > .timeline-panel { + float: right; +} + +.timeline > li.timeline-inverted > .timeline-panel:before { + right: auto; + left: -15px; + border-right-width: 15px; + border-left-width: 0; +} + +.timeline > li.timeline-inverted > .timeline-panel:after { + right: auto; + left: -14px; + border-right-width: 14px; + border-left-width: 0; +} + +.timeline-badge.primary { + background-color: #2e6da4 !important; +} + +.timeline-badge.success { + background-color: #3f903f !important; +} + +.timeline-badge.warning { + background-color: #f0ad4e !important; +} + +.timeline-badge.danger { + background-color: #d9534f !important; +} + +.timeline-badge.info { + background-color: #5bc0de !important; +} + +.timeline-title { + margin-top: 0; + color: inherit; +} + +.timeline-body > p, +.timeline-body > ul { + margin-bottom: 0; +} + +.timeline-body > p + p { + margin-top: 5px; +} + +@media(max-width:767px) { + ul.timeline:before { + left: 40px; + } + + ul.timeline > li > .timeline-panel { + width: calc(100% - 90px); + width: -moz-calc(100% - 90px); + width: -webkit-calc(100% - 90px); + } + + ul.timeline > li > .timeline-badge { + top: 16px; + left: 15px; + margin-left: 0; + } + + ul.timeline > li > .timeline-panel { + float: right; + } + + ul.timeline > li > .timeline-panel:before { + right: auto; + left: -15px; + border-right-width: 15px; + border-left-width: 0; + } + + ul.timeline > li > .timeline-panel:after { + right: auto; + left: -14px; + border-right-width: 14px; + border-left-width: 0; + } +} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/authority.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/authority.html new file mode 100644 index 0000000..5dbdded --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/authority.html @@ -0,0 +1,85 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ 授权规则 + + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + +
+ 资源名 + + 流控应用 + + 授权类型 + + 操作 +
{{ruleEntity.rule.resource}}{{ruleEntity.rule.limitApp }} + 白名单 + 黑名单 + + + +
+ + + + + + + + + + + + diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/client.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/client.html new file mode 100644 index 0000000..7fc751d --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/client.html @@ -0,0 +1,30 @@ +
+
+
+ +
+

未连接

+

连接中

+

已连接

+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/server.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/server.html new file mode 100644 index 0000000..8c04587 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster/server.html @@ -0,0 +1,29 @@ +
+
+
+ +
+

独立模式 (Alone)

+

嵌入模式 (Embedded)

+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_assign_manage.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_assign_manage.html new file mode 100644 index 0000000..550ff23 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_assign_manage.html @@ -0,0 +1,118 @@ +
+
+ {{app}} +
+
+ +
+
+
+
+
+
+ 集群限流 - 机器分配/管控 +
+ + +
+
+
+
+
+

{{loadError.message}}

+
+
+
+
+
+ + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+ + +
+ +
+
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_client_list.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_client_list.html new file mode 100644 index 0000000..b779e30 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_client_list.html @@ -0,0 +1,73 @@ +
+
+ {{app}} +
+ +
+ +
+
+
+
+
+
+ 集群限流 - Token Client 列表 +
+ + +
+
+
+
+
+

{{loadError.message}}

+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
Client IDServer IPServer 端口连接状态操作
{{clientVO.id}}{{clientVO.state.clientConfig.serverHost}}{{clientVO.state.clientConfig.serverPort}} + 未连接 + 连接中 + 已连接 + + +
+
+ +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_list.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_list.html new file mode 100644 index 0000000..d47b31f --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_list.html @@ -0,0 +1,96 @@ +
+
+ {{app}} +
+
+ + + Token Client 列表 + +
+
+ +
+
+
+
+
+
+ 集群限流 - Token Server 列表 + +
+ + +
+
+
+
+
+

{{loadError.message}}

+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Server IDPort命名空间集合运行模式总连接数QPS 总览操作
+ {{serverVO.id}} + {{serverVO.id}}(自主指定) + {{serverVO.port}} + {{serverVO.state.namespaceSetStr}} + 未知 + + 未知 + 嵌入模式 + 独立模式 + + {{serverVO.connectedCount}} + 未知 + + {{serverVO.state.requestLimitDataStr}} + 未知 + + + + +
+
+ +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_overview.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_overview.html new file mode 100644 index 0000000..4e411a2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_app_server_overview.html @@ -0,0 +1,88 @@ +
+
+ {{app}} +
+
+ +
+
+
+
+
+
+ 集群限流 - Token Server 总览 +
+ + +
+
+
+
+
+

{{loadError.message}}

+
+
+
+
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Server IDPort命名空间集合总连接数连接情况QPS 总览
{{serverVO.id}}{{serverVO.port}} + {{serverVO.state.namespaceSetStr}} + + {{serverVO.connectedCount}} + +

+ namespace: {{cg.namespace}}, 连接数: {{cg.connectedCount}}, clients: + {{generateConnectionSet(cg.connectionSet)}} +

+
+

+ namespace: {{crl.namespace}}, 当前 QPS: {{crl.currentQps}}, 最大允许 QPS: + {{crl.maxAllowedQps}} +

+
+
+ +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_single_config.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_single_config.html new file mode 100644 index 0000000..a82f1ab --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/cluster_single_config.html @@ -0,0 +1,95 @@ + +
+
+ {{app}} +
+
+ +
+
+
+
+
+
+ 集群限流 + +
+ +
+
+ + +
+
+
+
+
+

{{loadError.message}}

+
+
+
+
+
+ + +
+
+
+ +

Client

+

Server

+

未开启

+
+
+ +
+
+  Client   +  Server +
+
+
+
+ +
+ +
+
+
+
+
+

该机器未引入 Sentinel 集群限流客户端或服务端的相关依赖,请引入相关依赖。

+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/home.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/home.html new file mode 100644 index 0000000..9a81bf5 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/home.html @@ -0,0 +1,13 @@ +
+
+
+

欢迎使用 Sentinel 控制台

+
+ +
+ + +
+
+ +
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/main.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/main.html new file mode 100644 index 0000000..c5abed5 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dashboard/main.html @@ -0,0 +1,10 @@ +
+ +
+ + +
+
+
+ +
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/degrade.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/degrade.html new file mode 100644 index 0000000..c2b6752 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/degrade.html @@ -0,0 +1,98 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ 降级规则 + + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
+ 资源名 + + 降级策略 + + 阈值 + + 熔断时长(s) + + 操作 +
{{rule.resource}} + 慢调用比例 + 异常比例 + 异常数 + + {{rule.count}} + + {{rule.timeWindow}}s + + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/authority-rule-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/authority-rule-dialog.html new file mode 100644 index 0000000..bd69085 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/authority-rule-dialog.html @@ -0,0 +1,46 @@ +
+ {{authorityRuleDialog.title}} +
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  白名单   +  黑名单 +
+
+
+ +
+
+
+
+ + + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-client-config-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-client-config-dialog.html new file mode 100644 index 0000000..128ab78 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-client-config-dialog.html @@ -0,0 +1,40 @@ +
+ 修改 Token Client 配置 +
+
+
+
+
+ +
+

{{ccDialogData.clientId}}

+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+ + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-assign-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-assign-dialog.html new file mode 100644 index 0000000..350c2e4 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-assign-dialog.html @@ -0,0 +1,139 @@ +
+ {{serverAssignDialogData.title}} +
+
+
+
+
+
+ +
+

{{serverAssignDialogData.serverData.currentServer}}

+
+ + +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+  应用内机器   +  外部指定机器 +
+
+ +
+
+

若指定外部 server,请先在相应页面对外部 server 进行配置,然后在此页面指定。

+
+
+
+ +
+
+ +
+ +
+ + +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+ + +
+ +
+
+
+
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+
+ + + +
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-connection-detail-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-connection-detail-dialog.html new file mode 100644 index 0000000..afbf29a --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/cluster/cluster-server-connection-detail-dialog.html @@ -0,0 +1,37 @@ +
+ 连接详情 +
+
+
+
+
+ +
+

{{connectionDetailDialogData.serverData.id}}

+
+
+
+ +
+ + + + + + + + + + + + + + + + +
命名空间连接数连接详情
{{cg.namespace}}{{cg.connectedCount}}{{generateConnectionSet(cg.connectionSet)}}
+
+
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/confirm-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/confirm-dialog.html new file mode 100644 index 0000000..b7bf3d6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/confirm-dialog.html @@ -0,0 +1,20 @@ +
+ {{confirmDialog.title}} +
+
+
+

+ {{confirmDialog.attentionTitle}}: +
+
+ {{confirmDialog.attention}} +

+
+
+
+ + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html new file mode 100644 index 0000000..df241ec --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html @@ -0,0 +1,83 @@ +
+ {{degradeRuleDialog.title}} +
+
+
+
+
+ +
+ + +
+
+ + + + + + + + +
+ +
+
+  慢调用比例   +  异常比例   +  异常数 +
+
+
+ +
+ + + +
+ + + +
+
+ +
+ +
+
+
+ +
+ +
+
+ + s +
+
+ + +
+ +
+
+ + +
+
+
+
+ + + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html new file mode 100644 index 0000000..e68fb6b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html @@ -0,0 +1,148 @@ +
+ {{flowRuleDialog.title}} +
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  QPS   +  线程数 +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+
+ +
+
+  单机均摊   +  总体阈值 +
+
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+  直接   +  关联   +  链路   +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+  快速失败   +  Warm Up   +  排队等待 +
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ + + +
+
+
+
\ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html new file mode 100644 index 0000000..8c8d461 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html @@ -0,0 +1,49 @@ +
+ {{gatewayApiDialog.title}} +
+
+
+
+ +
+ + +
+
+ +
+ +
+
+  精确   +  前缀   +  正则   +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
+ + + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html new file mode 100644 index 0000000..ea744a7 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html @@ -0,0 +1,172 @@ +
+ {{gatewayFlowRuleDialog.title}} +
+
+
+
+
+ +
+
+  Route ID   +  Route ID   +  API 分组   +  API 分组   +
+
+
+ +
+ +
+ + + + + +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  Client IP   +  Remote Host   +  Header   +  URL 参数   +  Cookie   +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  精确   +  子串   +  正则   +
+
+ +
+ +
+
+ +
+ +
+
+  QPS   +  线程数   +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+  快速失败   +  匀速排队   +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+ + + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html new file mode 100644 index 0000000..02f00b0 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html @@ -0,0 +1,166 @@ +
+ {{paramFlowRuleDialog.title}} +
+
+
+
+
+ +
+ + +
+
+ +
+ +

QPS 模式

+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+
+ +
+
+  单机均摊   +  总体阈值 +
+
+
+
+ +
+ +
+
+ +  若选择,则 Token Server 不可用时将退化到单机限流 +
+
+
+ + +
+
+
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + +
参数值参数类型限流阈值操作
+

{{paramItem.classType}}

+
+ + + +
+
+
+ +
+
+ + + +
+
+
+
+ + + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html new file mode 100644 index 0000000..3dd9cd9 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html @@ -0,0 +1,58 @@ +
+ {{systemRuleDialog.title}} +
+
+
+
+ +
+ +
+
+ +  LOAD   + +  RT   + +  线程数   + +  入口 QPS   + +  CPU 使用率   + +
+
+ +  LOAD   + +  RT   + +  线程数   + +  入口 QPS   + +  CPU 使用率   + +
+
+
+
+ +
+ + + + + +
+
+
+
+
+
+ + +
+
+
+
diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v1.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v1.html new file mode 100644 index 0000000..22b79c6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v1.html @@ -0,0 +1,117 @@ +
+
+ {{app}} +
+
+ + + +
+
+ +
+ +
+
+
+
+
+ 流控规则 + + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ 资源名 + + 来源应用 + + 流控模式 + + 阈值类型 + + 阈值 + + 阈值模式 + + 流控效果 + + 操作 +
{{rule.resource}}{{rule.limitApp }} + 直接 + 关联 + 链路 + + {{rule.grade==0 ? '线程数' : 'QPS'}} + + {{rule.count}} + + {{generateThresholdTypeShow(rule)}} + + 快速失败 + Warm Up + 排队等待 + 预热排队 + + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v2.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v2.html new file mode 100644 index 0000000..7e0dcc8 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/flow_v2.html @@ -0,0 +1,113 @@ +
+
+ {{app}} +
+
+ + + 回到单机页面 + +
+
+ +
+ +
+
+
+
+
+ 流控规则 + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ 资源名 + + 来源应用 + + 流控模式 + + 阈值类型 + + 阈值 + + 阈值模式 + + 流控效果 + + 操作 +
{{rule.resource}}{{rule.limitApp }} + 直接 + 关联 + 链路 + + {{rule.grade == 0 ? '线程数' : 'QPS'}} + + {{rule.count}} + + {{generateThresholdTypeShow(rule)}} + + 快速失败 + Warm Up + 排队等待 + 预热排队 + + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/api.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/api.html new file mode 100644 index 0000000..b4e101c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/api.html @@ -0,0 +1,87 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ API 分组管理 + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + +
+ API 名称 + + 匹配模式 + + 匹配串 + + 操作 +
{{api.apiName}} + 精确 + 前缀 + 正则 + {{api.pattern}} + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/flow.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/flow.html new file mode 100644 index 0000000..62708c4 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/flow.html @@ -0,0 +1,94 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ 网关流控规则 + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ API 名称 + + API 类型 + + 阈值类型 + + 单机阈值 + + 操作 +
{{rule.resource}} + Route ID + API 分组 + + QPS + 线程数 + {{rule.count}} + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/identity.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/identity.html new file mode 100644 index 0000000..0736adc --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/gateway/identity.html @@ -0,0 +1,98 @@ +
+
+ {{app}} +
+
+ +
+ +
+
+
+
+
+ 请求链路 + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ API 名称 + + API 类型 + 通过 QPS拒绝 QPS线程数平均 RT分钟通过分钟拒绝操作
+ {{resource.resource}} + + Route ID + 自定义 API + {{resource.passQps}}{{resource.blockQps}}{{resource.threadNum}}{{resource.averageRt}}{{resource.oneMinutePass}} + {{resource.oneMinuteBlock}} {{resource.oneMinuteBlock}} +
+ + +
+
+
+ + + +
+ +
+ +
+ +
+ \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/identity.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/identity.html new file mode 100644 index 0000000..1dcf6e9 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/identity.html @@ -0,0 +1,110 @@ +
+
+ {{app}} +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+ 簇点链路 + + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 资源名 + 通过QPS拒绝QPS线程数平均RT分钟通过分钟拒绝操作
+ + + {{resource.resource}} + + {{resource.passQps}}{{resource.blockQps}}{{resource.threadNum}}{{resource.averageRt}}{{resource.oneMinutePass}} + {{resource.oneMinuteBlock}} {{resource.oneMinuteBlock}} +
+ + + + +
+
+
+ + + +
+ +
+ +
+ +
+ \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/login.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/login.html new file mode 100644 index 0000000..b507978 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/login.html @@ -0,0 +1,34 @@ +
+
+ Sentinel Logo + +
+
+
+
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/machine.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/machine.html new file mode 100644 index 0000000..6cfcff9 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/machine.html @@ -0,0 +1,76 @@ +
+
+ {{app}} +
+
+ + + + + +
+
+
+
+
+
+ 机器列表 + 实例总数 {{machines.length}}, 健康 {{healthyCount}}, 失联 {{machines.length - healthyCount}}. + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
机器名IP 地址端口号Sentinel 客户端版本健康状态心跳时间操作
{{entry.hostname}}{{entry.ip}} {{entry.port}} {{entry.version}} 健康失联{{entry.lastHeartbeat | date: 'yyyy/MM/dd HH:mm:ss'}} + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/metric.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/metric.html new file mode 100644 index 0000000..d827c9b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/metric.html @@ -0,0 +1,120 @@ +
+
+ {{app}} +
+
+ +
+
+
+
+
+
+ + + + {{metricTypeDesc}} 实时监控 + + + + + + + + +
+ + +
+
+
+
+ + +
+
+  {{metric.resource}} + + + +
+ + +
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
时间通过 QPS拒绝QPS响应时间(ms)
{{tableObj.timestamp | date: 'HH:mm:ss'}}{{tableObj.passQps | number : 1}}{{tableObj.blockQps | number : 1}}{{tableObj.rt | number : 1}}
-
+
+
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
  • +
    + + + +
    +
    + +
    + +
    + +
    diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/pagination.tpl.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/pagination.tpl.html new file mode 100644 index 0000000..6ebbee2 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/pagination.tpl.html @@ -0,0 +1,18 @@ + diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/param_flow.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/param_flow.html new file mode 100644 index 0000000..c94219b --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/param_flow.html @@ -0,0 +1,118 @@ +
    +
    + {{app}} +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + 热点参数限流规则 + + +
    + +
    +
    + + +
    +
    +
    +
    +
    +

    {{loadError.message}}

    +
    +
    +
    +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + 资源名 + + 参数索引 + + 流控模式 + + 阈值 + + 是否集群 + + 例外项数目 + + 操作 +
    {{ruleEntity.rule.resource}}{{ruleEntity.rule.paramIdx}} + {{ruleEntity.rule.grade == 1 ? 'QPS' : '未知'}} + + {{ruleEntity.rule.count}} + + + + + {{ruleEntity.rule.paramFlowItemList == undefined ? 0 : ruleEntity.rule.paramFlowItemList.length}} + + + +
    +
    + + + + + +
    + +
    + +
    + +
    + + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/system.html b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/system.html new file mode 100644 index 0000000..6f4e4b8 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/app/views/system.html @@ -0,0 +1,92 @@ +
    +
    + {{app}} +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + 系统规则 + + + +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + +
    + 阈值类型 + + 单机阈值 + + 操作 +
    + 系统 load + 平均 RT + 并发数 + 入口 QPS + CPU 使用率 + + {{rule.highestSystemLoad}} + {{rule.avgRt}} + {{rule.maxThread}} + {{rule.qps}} + {{rule.highestCpuUsage}} + + + +
    +
    + + + +
    + +
    + +
    + +
    + diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/assets/img/sentinel-logo.png b/chushang-visual/chushang-sentinel/src/main/webapp/resources/assets/img/sentinel-logo.png new file mode 100644 index 0000000..e69de29 diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/css/app.css b/chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/css/app.css new file mode 100644 index 0000000..a9e9075 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/css/app.css @@ -0,0 +1,5 @@ +.chat,.timeline{list-style:none}#loading-bar,#loading-bar-spinner{pointer-events:none;-webkit-pointer-events:none;-webkit-transition:350ms linear all;-moz-transition:350ms linear all;-o-transition:350ms linear all;transition:350ms linear all}#loading-bar-spinner.ng-enter,#loading-bar-spinner.ng-leave.ng-leave-active,#loading-bar.ng-enter,#loading-bar.ng-leave.ng-leave-active{opacity:0}#loading-bar-spinner.ng-enter.ng-enter-active,#loading-bar-spinner.ng-leave,#loading-bar.ng-enter.ng-enter-active,#loading-bar.ng-leave{opacity:1}#loading-bar .bar{-webkit-transition:width 350ms;-moz-transition:width 350ms;-o-transition:width 350ms;transition:width 350ms;background:#29d;position:fixed;z-index:10002;top:0;left:0;width:100%;height:2px;border-bottom-right-radius:1px;border-top-right-radius:1px}#loading-bar .peg{position:absolute;width:70px;right:0;top:0;height:2px;opacity:.45;-moz-box-shadow:#29d 1px 0 6px 1px;-ms-box-shadow:#29d 1px 0 6px 1px;-webkit-box-shadow:#29d 1px 0 6px 1px;box-shadow:#29d 1px 0 6px 1px;-moz-border-radius:100%;-webkit-border-radius:100%;border-radius:100%}#loading-bar-spinner{display:block;position:fixed;z-index:10002;top:10px;left:10px}#loading-bar-spinner .spinner-icon{width:14px;height:14px;border:2px solid transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:loading-bar-spinner .4s linear infinite;-moz-animation:loading-bar-spinner .4s linear infinite;-ms-animation:loading-bar-spinner .4s linear infinite;-o-animation:loading-bar-spinner .4s linear infinite;animation:loading-bar-spinner .4s linear infinite}@-webkit-keyframes loading-bar-spinner{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes loading-bar-spinner{0%{-moz-transform:rotate(0);transform:rotate(0)}100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}@-o-keyframes loading-bar-spinner{0%{-o-transform:rotate(0);transform:rotate(0)}100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes loading-bar-spinner{0%{-ms-transform:rotate(0);transform:rotate(0)}100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-bar-spinner{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.bootstrap-switch{display:inline-block;direction:ltr;cursor:pointer;border-radius:4px;border:1px solid #ccc;position:relative;text-align:left;overflow:hidden;line-height:8px;z-index:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.bootstrap-switch .bootstrap-switch-container{display:inline-block;top:0;border-radius:4px;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;cursor:pointer;display:table-cell;vertical-align:middle;padding:6px 12px;font-size:14px;line-height:20px}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{color:#fff;background:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{color:#fff;background:#5bc0de}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#f0ad4e;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{color:#fff;background:#d9534f}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{color:#000;background:#eee}.bootstrap-switch .bootstrap-switch-label{text-align:center;margin-top:-1px;margin-bottom:-1px;z-index:100;color:#333;background:#fff}.bootstrap-switch span::before{content:"\200b"}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:3px;border-top-left-radius:3px}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch input[type=radio],.bootstrap-switch input[type=checkbox]{position:absolute!important;top:0;left:0;margin:0;z-index:-1;opacity:0;filter:alpha(opacity=0);visibility:hidden}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{padding:1px 5px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{padding:5px 10px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{padding:6px 16px;font-size:18px;line-height:1.3333333}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default!important}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{opacity:.5;filter:alpha(opacity=50);cursor:default!important}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 3px 3px 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:3px 0 0 3px}.bootstrap-switch.bootstrap-switch-focused{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:3px;border-top-left-radius:3px}.ngdialog,.ngdialog-overlay{position:fixed;top:0;right:0;bottom:0;left:0}@-webkit-keyframes ngdialog-fadeout{0%{opacity:1}100%{opacity:0}}@keyframes ngdialog-fadeout{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes ngdialog-fadein{0%{opacity:0}100%{opacity:1}}@keyframes ngdialog-fadein{0%{opacity:0}100%{opacity:1}}.ngdialog{box-sizing:border-box;overflow:auto;-webkit-overflow-scrolling:touch;z-index:10000}.ngdialog *,.ngdialog :after,.ngdialog :before{box-sizing:inherit}.ngdialog.ngdialog-disabled-animation,.ngdialog.ngdialog-disabled-animation .ngdialog-content,.ngdialog.ngdialog-disabled-animation .ngdialog-overlay{-webkit-animation:none!important;animation:none!important}.ngdialog-overlay{background:rgba(0,0,0,.4);-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadein .5s;animation:ngdialog-fadein .5s}.ngdialog-no-overlay{pointer-events:none}.ngdialog.ngdialog-closing .ngdialog-overlay{-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadeout .5s;animation:ngdialog-fadeout .5s}.ngdialog-content{background:#fff;-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadein .5s;animation:ngdialog-fadein .5s;pointer-events:all}.ngdialog.ngdialog-closing .ngdialog-content{-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadeout .5s;animation:ngdialog-fadeout .5s}.ngdialog-close:before{font-family:Helvetica,Arial,sans-serif;content:'\00D7';cursor:pointer}body.ngdialog-open,html.ngdialog-open{overflow:hidden}@-webkit-keyframes ngdialog-flyin{0%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes ngdialog-flyin{0%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes ngdialog-flyout{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes ngdialog-flyout{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}}.ngdialog.ngdialog-theme-default{padding-bottom:160px;padding-top:160px}.ngdialog.ngdialog-theme-default.ngdialog-closing .ngdialog-content{-webkit-animation:ngdialog-flyout .5s;animation:ngdialog-flyout .5s}.ngdialog.ngdialog-theme-default .ngdialog-content{-webkit-animation:ngdialog-flyin .5s;animation:ngdialog-flyin .5s;background:#f0f0f0;border-radius:5px;color:#444;font-family:Helvetica,sans-serif;font-size:1.1em;line-height:1.5em;margin:0 auto;max-width:100%;padding:1em;position:relative;width:450px}.ngdialog.ngdialog-theme-default .ngdialog-close{border-radius:5px;cursor:pointer;position:absolute;right:0;top:0}.ngdialog.ngdialog-theme-default .ngdialog-close:before{background:0 0;border-radius:3px;color:#bbb;content:'\00D7';font-size:26px;font-weight:400;height:30px;line-height:26px;position:absolute;right:3px;text-align:center;top:3px;width:30px}.ngdialog.ngdialog-theme-default .ngdialog-close:active:before,.ngdialog.ngdialog-theme-default .ngdialog-close:hover:before{color:#777}.ngdialog.ngdialog-theme-default .ngdialog-message{margin-bottom:.5em}.ngdialog.ngdialog-theme-default .ngdialog-input{margin-bottom:1em}.ngdialog.ngdialog-theme-default .ngdialog-input input[type=text],.ngdialog.ngdialog-theme-default .ngdialog-input input[type=password],.ngdialog.ngdialog-theme-default .ngdialog-input input[type=email],.ngdialog.ngdialog-theme-default .ngdialog-input input[type=url],.ngdialog.ngdialog-theme-default .ngdialog-input textarea{background:#fff;border:0;border-radius:3px;font-family:inherit;font-size:inherit;font-weight:inherit;margin:0 0 .25em;min-height:2.5em;padding:.25em .67em;width:100%}.ngdialog.ngdialog-theme-default .ngdialog-input input[type=text]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input input[type=password]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input input[type=email]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input input[type=url]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input textarea:focus{box-shadow:inset 0 0 0 2px #8dbdf1;outline:0}.ngdialog.ngdialog-theme-default .ngdialog-buttons:after{content:'';display:table;clear:both}.ngdialog.ngdialog-theme-default .ngdialog-button{border:0;border-radius:3px;cursor:pointer;float:right;font-family:inherit;font-size:.8em;letter-spacing:.1em;line-height:1em;margin:0 0 0 .5em;padding:.75em 2em;text-transform:uppercase}.ngdialog.ngdialog-theme-default .ngdialog-button:focus{-webkit-animation:ngdialog-pulse 1.1s infinite;animation:ngdialog-pulse 1.1s infinite;outline:0}.btn:active,.btn:focus,.selectize-input>input:focus{outline:0!important}@media (max-width:568px){.ngdialog.ngdialog-theme-default .ngdialog-button:focus{-webkit-animation:none;animation:none}}.ngdialog.ngdialog-theme-default .ngdialog-button.ngdialog-button-primary{background:#3288e6;color:#fff}.ngdialog.ngdialog-theme-default .ngdialog-button.ngdialog-button-secondary{background:#e0e0e0;color:#777}.datetimepicker{border-radius:4px;direction:ltr;display:block;margin-top:1px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:320px}.datetimepicker>div{display:none}.datetimepicker .hour,.datetimepicker .minute{height:34px;line-height:34px;margin:0;width:25%}.datetimepicker .table{margin:0}.datetimepicker .table td,.datetimepicker .table th{border:0;border-radius:4px;height:20px;text-align:center}.datetimepicker .day:hover,.datetimepicker .hour:hover,.datetimepicker .left:hover,.datetimepicker .minute:hover,.datetimepicker .right:hover,.datetimepicker .switch:hover{background:#eee;cursor:pointer}.datetimepicker .disabled,.datetimepicker .disabled:hover{background:0 0;color:#ebebeb;cursor:default}.datetimepicker .active,.datetimepicker .active.disabled,.datetimepicker .active.disabled:hover,.datetimepicker .active:hover{background-color:#04c;background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;color:#fff;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#08c', endColorstr='#04c', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datetimepicker .current,.datetimepicker .current.disabled,.datetimepicker .current.disabled:hover,.datetimepicker .current:hover{background-color:#e5e5e5}.datetimepicker .active.active,.datetimepicker .active.disabled,.datetimepicker .active.disabled.active,.datetimepicker .active.disabled.disabled,.datetimepicker .active.disabled:active,.datetimepicker .active.disabled:hover,.datetimepicker .active.disabled:hover.active,.datetimepicker .active.disabled:hover.disabled,.datetimepicker .active.disabled:hover:active,.datetimepicker .active.disabled:hover:hover,.datetimepicker .active:active,.datetimepicker .active:hover,.datetimepicker .active:hover.active,.datetimepicker .active:hover.disabled,.datetimepicker .active:hover:active,.datetimepicker .active:hover:hover,.datetimepicker span.active.disabled:hover[disabled],.datetimepicker span.active.disabled[disabled],.datetimepicker span.active:hover[disabled],.datetimepicker span.active[disabled],.datetimepicker td.active.disabled:hover[disabled],.datetimepicker td.active.disabled[disabled],.datetimepicker td.active:hover[disabled],.datetimepicker td.active[disabled]{background-color:#04c}.datetimepicker span{border-radius:4px;cursor:pointer;display:block;float:left;height:54px;line-height:54px;margin:1%;width:23%}.datetimepicker span:hover{background:#eee}.datetimepicker .future,.datetimepicker .past{color:#999}.ui-notification{position:fixed;z-index:9999;width:300px;-webkit-transition:all ease .5s;-o-transition:all ease .5s;transition:all ease .5s;color:#fff;border-radius:0;background:#337ab7;box-shadow:5px 5px 10px rgba(0,0,0,.3)}.ui-notification.clickable{cursor:pointer}.ui-notification.clickable:hover{opacity:.7}.ui-notification.killed{-webkit-transition:opacity ease 1s;-o-transition:opacity ease 1s;transition:opacity ease 1s;opacity:0}.ui-notification>h3{font-size:14px;font-weight:700;display:block;margin:10px 10px 0;padding:0 0 5px;text-align:left;border-bottom:1px solid rgba(255,255,255,.3)}.ui-notification a{color:#fff}.ui-notification a:hover{text-decoration:underline}.ui-notification>.message{margin:10px}.ui-notification.warning{color:#fff;background:#f0ad4e}.ui-notification.error{color:#fff;background:#d9534f}.ui-notification.success{color:#fff;background:#5cb85c}.ui-notification.info{color:#fff;background:#5bc0de}table.rz-table{table-layout:fixed;border-collapse:collapse}table.rz-table th{position:relative;min-width:25px}table.rz-table th .rz-handle{width:10px;height:100%;position:absolute;top:0;right:0;cursor:ew-resize!important}table.rz-table th .rz-handle.rz-handle-active{border-right:1px dotted #000}.selectize-control.plugin-drag_drop.multi>.selectize-input>div.ui-sortable-placeholder{visibility:visible!important;background:#f2f2f2!important;background:rgba(0,0,0,.06)!important;border:0!important;-webkit-box-shadow:inset 0 0 12px 4px #fff;box-shadow:inset 0 0 12px 4px #fff}.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after{content:'!';visibility:hidden}.selectize-control.plugin-drag_drop .ui-sortable-helper{-webkit-box-shadow:0 2px 5px rgba(0,0,0,.2);box-shadow:0 2px 5px rgba(0,0,0,.2)}.selectize-dropdown-header{position:relative;padding:5px 8px;border-bottom:1px solid #d0d0d0;background:#f8f8f8;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0}.selectize-dropdown-header-close{position:absolute;right:8px;top:50%;color:#303030;opacity:.4;margin-top:-12px;line-height:20px;font-size:20px!important}.selectize-dropdown-header-close:hover{color:#000}.selectize-dropdown.plugin-optgroup_columns .optgroup{border-right:1px solid #f2f2f2;border-top:0 none;float:left;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.selectize-control.plugin-remove_button [data-value] .remove,.selectize-input{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;display:inline-block}.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child{border-right:0 none}.selectize-dropdown.plugin-optgroup_columns .optgroup:before{display:none}.selectize-dropdown.plugin-optgroup_columns .optgroup-header{border-top:0 none}.selectize-control.plugin-remove_button [data-value]{position:relative;padding-right:24px!important}.selectize-control.plugin-remove_button [data-value] .remove{z-index:1;position:absolute;top:0;right:0;bottom:0;width:17px;text-align:center;font-weight:700;font-size:12px;color:inherit;text-decoration:none;vertical-align:middle;padding:2px 0 0;border-left:1px solid #d0d0d0;-webkit-border-radius:0 2px 2px 0;-moz-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0;box-sizing:border-box}.selectize-control.plugin-remove_button [data-value] .remove:hover{background:rgba(0,0,0,.05)}.selectize-control.plugin-remove_button [data-value].active .remove{border-left-color:#cacaca}.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover{background:0 0}.selectize-control.plugin-remove_button .disabled [data-value] .remove{border-left-color:#fff}.selectize-control.plugin-remove_button .remove-single{position:absolute;right:0;top:0;font-size:23px}.selectize-control,.selectize-input{position:relative}.selectize-dropdown,.selectize-input,.selectize-input input{color:#303030;font-family:inherit;font-size:13px;line-height:18px;-webkit-font-smoothing:inherit}.selectize-control.single .selectize-input.input-active,.selectize-input{background:#fff;cursor:text;display:inline-block}.selectize-input{border:1px solid #d0d0d0;padding:8px;width:100%;overflow:hidden;z-index:1;box-sizing:border-box;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.1);box-shadow:inset 0 1px 1px rgba(0,0,0,.1);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.selectize-control.multi .selectize-input.has-items{padding:6px 8px 3px}.selectize-input.full{background-color:#fff}.selectize-input.disabled,.selectize-input.disabled *{cursor:default!important}.selectize-input.focus{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.15);box-shadow:inset 0 1px 2px rgba(0,0,0,.15)}.selectize-input.dropdown-active{-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0}.selectize-input>*{vertical-align:baseline;display:-moz-inline-stack;display:inline-block;zoom:1}.selectize-control.multi .selectize-input>div{cursor:pointer;margin:0 3px 3px 0;padding:2px 6px;background:#f2f2f2;color:#303030;border:0 solid #d0d0d0}.selectize-control.multi .selectize-input>div.active{background:#e8e8e8;color:#303030;border:0 solid #cacaca}.selectize-control.multi .selectize-input.disabled>div,.selectize-control.multi .selectize-input.disabled>div.active{color:#7d7d7d;background:#fff;border:0 solid #fff}.selectize-input>input{display:inline-block!important;padding:0!important;min-height:0!important;max-height:none!important;max-width:100%!important;margin:0 2px 0 0!important;text-indent:0!important;border:0!important;background:0 0!important;line-height:inherit!important;-webkit-user-select:auto!important;-webkit-box-shadow:none!important;box-shadow:none!important}.selectize-input>input::-ms-clear{display:none}.selectize-input::after{content:' ';display:block;clear:left}.selectize-input.dropdown-active::before{content:' ';display:block;position:absolute;background:#f0f0f0;height:1px;bottom:0;left:0;right:0}.selectize-dropdown{position:absolute;z-index:10;border:1px solid #d0d0d0;background:#fff;margin:-1px 0 0;border-top:0 none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.1);-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.selectize-dropdown [data-selectable]{cursor:pointer;overflow:hidden}.selectize-dropdown [data-selectable] .highlight{background:rgba(125,168,208,.2);-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px}.selectize-dropdown .optgroup-header,.selectize-dropdown .option{padding:5px 8px}.selectize-dropdown .option,.selectize-dropdown [data-disabled],.selectize-dropdown [data-disabled] [data-selectable].option{cursor:inherit;opacity:.5}.selectize-dropdown [data-selectable].option{opacity:1}.selectize-dropdown .optgroup:first-child .optgroup-header{border-top:0 none}.selectize-dropdown .optgroup-header{color:#303030;background:#fff;cursor:default}.selectize-dropdown .active{background-color:#f5fafd;color:#495c68}.selectize-dropdown .active.create{color:#495c68}.selectize-dropdown .create{color:rgba(48,48,48,.5)}.selectize-dropdown-content{overflow-y:auto;overflow-x:hidden;max-height:200px;-webkit-overflow-scrolling:touch}.selectize-control.single .selectize-input,.selectize-control.single .selectize-input input{cursor:pointer}.selectize-control.single .selectize-input.input-active,.selectize-control.single .selectize-input.input-active input{cursor:text}.selectize-control.single .selectize-input:after{content:' ';display:block;position:absolute;top:50%;right:15px;margin-top:-3px;width:0;height:0;border-style:solid;border-width:5px 5px 0;border-color:grey transparent transparent}.selectize-control.single .selectize-input.dropdown-active:after{margin-top:-4px;border-width:0 5px 5px;border-color:transparent transparent grey}.selectize-control.rtl.single .selectize-input:after{left:15px;right:auto}.selectize-control.rtl .selectize-input>input{margin:0 4px 0 -2px!important}.selectize-control .selectize-input.disabled{opacity:.5;background-color:#fafafa}/*! + * Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com) + * Code licensed under the Apache License v2.0. + * For details, see http://www.apache.org/licenses/LICENSE-2.0. + */body{background-color:#f8f8f8}.example{padding:.625rem 1.825rem .625rem 2.5rem;border:1px dashed #ccc;position:relative;margin:0 0 .625rem;background-color:#fff}dl dd,dl dt{line-height:1.25rem}dl dt{font-style:normal;font-weight:700}dl dd{margin-left:.9375rem}dl.horizontal dt{float:left;width:10rem;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}dl.horizontal dd{margin-left:11.25rem}#wrapper{width:100%}#page-wrapper{min-height:568px;background-color:#fff}@media(min-width:768px){#page-wrapper{position:inherit;margin:0 0 0 250px;padding:0 30px;border-left:1px solid #e7e7e7}}.navbar-top-links{margin-right:0}.navbar-top-links li{display:inline-block}.flot-chart,.navbar-top-links .dropdown-menu li{display:block}.navbar-top-links li:last-child{margin-right:15px}.navbar-top-links li a{padding:15px;min-height:50px}.navbar-top-links .dropdown-menu li:last-child{margin-right:0}.navbar-top-links .dropdown-menu li a{padding:3px 20px;min-height:0}.navbar-top-links .dropdown-menu li a div{white-space:normal}.navbar-top-links .dropdown-alerts,.navbar-top-links .dropdown-messages,.navbar-top-links .dropdown-tasks{width:310px;min-width:0}.navbar-top-links .dropdown-messages{margin-left:5px}.navbar-top-links .dropdown-tasks{margin-left:-59px}.navbar-top-links .dropdown-alerts{margin-left:-123px}.navbar-top-links .dropdown-user{right:0;left:auto}.sidebar .sidebar-search{padding:15px}.sidebar ul li{border-bottom:1px solid #e7e7e7}.sidebar ul li a.active{background-color:#fff;color:#fff}.sidebar .arrow{float:right}.sidebar .fa.arrow:before{content:"\f104"}.sidebar .active>a>.fa.arrow:before{content:"\f107"}.sidebar .nav-second-level li,.sidebar .nav-third-level li{border-bottom:0!important}.sidebar .nav-second-level li a{padding-left:37px}.sidebar .nav-third-level li a{padding-left:52px}@media(min-width:768px){.sidebar{z-index:1;position:absolute;width:250px;margin-top:51px}.navbar-top-links .dropdown-alerts,.navbar-top-links .dropdown-messages,.navbar-top-links .dropdown-tasks{margin-left:auto}}.btn-outline{color:inherit;background-color:transparent;transition:all .5s}.btn-primary.btn-outline{color:#428bca}.btn-success.btn-outline{color:#5cb85c}.btn-info.btn-outline{color:#5bc0de}.btn-warning.btn-outline{color:#f0ad4e}.btn-danger.btn-outline{color:#d9534f}.btn-danger.btn-outline:hover,.btn-info.btn-outline:hover,.btn-primary.btn-outline:hover,.btn-success.btn-outline:hover,.btn-warning.btn-outline:hover{color:#fff}.chat{margin:0;padding:0}.chat li{margin-bottom:10px;padding-bottom:5px;border-bottom:1px dotted #999}.chat li.left .chat-body{margin-left:60px}.chat li.right .chat-body{margin-right:60px}.chat li .chat-body p{margin:0}.chat .glyphicon,.panel .slidedown .glyphicon{margin-right:5px}.chat-panel .panel-body{height:350px;overflow-y:scroll}.login-panel{margin-top:25%}.flot-chart{height:400px}.flot-chart-content{width:100%;height:100%}.dataTables_wrapper{position:relative;clear:both}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_desc_disabled{background:0 0}table.dataTable thead .sorting_asc:after{content:"\f0de";float:right;font-family:fontawesome}table.dataTable thead .sorting_desc:after{content:"\f0dd";float:right;font-family:fontawesome}table.dataTable thead .sorting:after{content:"\f0dc";float:right;font-family:fontawesome;color:rgba(50,50,50,.5)}.btn-circle{width:30px;height:30px;padding:6px 0;border-radius:15px;text-align:center;font-size:12px;line-height:1.428571429}.btn-circle.btn-lg{width:50px;height:50px;padding:10px 16px;border-radius:25px;font-size:18px;line-height:1.33}.btn-circle.btn-xl{width:70px;height:70px;padding:10px 16px;border-radius:35px;font-size:24px;line-height:1.33}.show-grid [class^=col-]{padding-top:10px;padding-bottom:10px;border:1px solid #ddd;background-color:#eee!important}.show-grid{margin:15px 0}.huge{font-size:40px}.panel-green{border-color:#5cb85c}.panel-green .panel-heading{border-color:#5cb85c;color:#fff;background-color:#5cb85c}.panel-green a{color:#5cb85c}.panel-green a:hover{color:#3d8b3d}.panel-red{border-color:#d9534f}.panel-red .panel-heading{border-color:#d9534f;color:#fff;background-color:#d9534f}.panel-red a{color:#d9534f}.panel-red a:hover{color:#b52b27}.panel-yellow{border-color:#f0ad4e}.panel-yellow .panel-heading{border-color:#f0ad4e;color:#fff;background-color:#f0ad4e}.panel-yellow a{color:#f0ad4e}.panel-yellow a:hover{color:#df8a13}.timeline{position:relative;padding:20px 0}.timeline:before{content:" ";position:absolute;top:0;bottom:0;left:50%;width:3px;margin-left:-1.5px;background-color:#eee}.timeline>li{position:relative;margin-bottom:20px}.timeline>li:after,.timeline>li:before{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-panel{float:left;position:relative;width:46%;padding:20px;border:1px solid #d4d4d4;border-radius:2px;-webkit-box-shadow:0 1px 6px rgba(0,0,0,.175);box-shadow:0 1px 6px rgba(0,0,0,.175)}.timeline>li>.timeline-panel:before{content:" ";display:inline-block;position:absolute;top:26px;right:-15px;border-top:15px solid transparent;border-right:0 solid #ccc;border-bottom:15px solid transparent;border-left:15px solid #ccc}.timeline>li>.timeline-panel:after{content:" ";display:inline-block;position:absolute;top:27px;right:-14px;border-top:14px solid transparent;border-right:0 solid #fff;border-bottom:14px solid transparent;border-left:14px solid #fff}.timeline>li>.timeline-badge{z-index:100;position:absolute;top:16px;left:50%;width:50px;height:50px;margin-left:-25px;border-radius:50%;text-align:center;font-size:1.4em;line-height:50px;color:#fff;background-color:#999}.timeline>li.timeline-inverted>.timeline-panel{float:right}.timeline>li.timeline-inverted>.timeline-panel:before{right:auto;left:-15px;border-right-width:15px;border-left-width:0}.timeline>li.timeline-inverted>.timeline-panel:after{right:auto;left:-14px;border-right-width:14px;border-left-width:0}.timeline-badge.primary{background-color:#2e6da4!important}.timeline-badge.success{background-color:#3f903f!important}.timeline-badge.warning{background-color:#f0ad4e!important}.timeline-badge.danger{background-color:#d9534f!important}.timeline-badge.info{background-color:#5bc0de!important}.timeline-title{margin-top:0;color:inherit}.timeline-body>p,.timeline-body>ul{margin-bottom:0}.timeline-body>p+p{margin-top:5px}@media(max-width:767px){ul.timeline:before{left:40px}ul.timeline>li>.timeline-panel{width:calc(100% - 90px);width:-moz-calc(100% - 90px);width:-webkit-calc(100% - 90px);float:right}ul.timeline>li>.timeline-badge{top:16px;left:15px;margin-left:0}ul.timeline>li>.timeline-panel:before{right:auto;left:-15px;border-right-width:15px;border-left-width:0}ul.timeline>li>.timeline-panel:after{right:auto;left:-14px;border-right-width:14px;border-left-width:0}}.header,.jumbotron{border-bottom:1px solid #e5e5e5}.btn{height:32px}.width-200{max-width:200px}.width-300,.witdh-300{max-width:300px}body{padding:0}.footer,.header,.marketing{padding-left:15px;padding-right:15px}.header{margin-bottom:10px}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.card .detail,.card .detail-brand{line-height:98px;text-align:center}.footer{padding-top:19px;color:#777;border-top:1px solid #e5e5e5}.container-narrow>hr{margin:30px 0}.jumbotron{text-align:center}.jumbotron .btn{font-size:21px;padding:14px 24px}.marketing{margin:40px 0}.marketing p+h4{margin-top:28px}@media screen and (min-width:768px){.container{width:inherit;margin-left:60px;margin-right:5px}.footer,.header,.marketing{padding-left:0;padding-right:0}.header{margin-bottom:30px}.jumbotron{border-bottom:0}}.navbar-inverse .navbar-nav>li>a{color:#b0ddce;font-size:15px}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#1b926c}@media (min-width:900px){.navbar-right,.navbar-right~.navbar-right{margin-right:0}.navbar-left{float:left!important}.navbar-right{float:right!important}}.dropdown-menu{min-width:100px!important}.nav-sidebar li.active a{background:#DDD}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background:#1d9d74;color:#fff}.broadcast-message,.broadcast-message-preview{padding:10px;text-align:center;background:#555;color:#BBB;margin-top:50px}.card{position:relative;border:1px solid #d9d9d9;color:#666;background-color:#fff;width:100%;border-radius:5px}.card .card-header,.tools-header{border-top-left-radius:4px;border-top-right-radius:4px}.card .card-header{padding:9px 0;height:40px;background:#555;color:#fff;text-align:center}.card .card-body{padding:12px 10px}.card .card-footer{height:20px;font-size:10px;color:#777;margin:-15px 20px 5px}.card .detail-brand{float:left;width:30%;font-size:30px;color:#fff}.card .default{background:#1d9d74}.card .info{background:#6EBEE7}.card .warn{background:#ED7F54}.card .danger{background:#6583BE}.card .detail .text-default{color:#1d9d74}.card .detail .text-info{color:#6EBEE7}.card .detail .text-warn{color:#ED7F54}.card .detail .text-danger{color:#6583BE}.card .detail{float:right;width:70%}.card .detail .text{font-size:12px}.card .detail .number{font-size:30px;font-weight:500}.h100{height:100px}.inline{display:inline}.separator{height:1px;background-color:#e5e5e5;margin-top:10px}.card>.card-body>table>tbody>tr>td,.card>.card-body>table>thead>tr>td{word-wrap:break-word;word-break:break-all}.card>.card-body>table>thead>tr>td{font-weight:500;font-size:13px;text-align:center}.card>.card-body>table>thead>tr>td>span{font-weight:500;font-size:10px}.card>.card-body>table>tbody>tr>td{font-size:12px;text-align:center}.card>.card-body>table>tbody>tr>td>a{color:#666}.thumbnails>.card>.card-body>table>tbody>tr>td,.thumbnails>.card>.card-body>table>thead>tr>td{font-size:12px;color:#777;word-wrap:break-word;word-break:break-all}.thumbnails>.card>.card-body>table>thead>tr>td:nth-child(n+2){text-align:center}.thumbnails>.card>.card-body>table>tbody>tr>td:nth-child(n+2){font-weight:700;text-align:center}.thumbnails>.card>.card-body>table>tbody>tr>td:nth-child(1),.thumbnails>.card>.card-body>table>thead>tr>td:nth-child(1){text-align:left}.tools-header{background:#f5f5f5;padding:9px 0;height:40px}.tools-header .brand{font-size:13px;margin:2px 10px;font-weight:700;float:left}.tools-header .brand>a{color:#666}.tools-header>a,.tools-header>button,.tools-header>select{float:right;max-width:80px;margin:1px 10px;height:25px;padding:0 10px;line-height:25px;color:#666}.tools-header .paged{margin-right:0}.btn.btn-danger-tag{color:#fff;background-color:#d9534f;border-color:#d43f3a;line-height:1px;font-size:11px;padding:4px}.btn.btn-danger{color:#333;background-color:#fff;border-color:#ccc}.btn.btn-danger:active,.btn.btn-danger:focus,.btn.btn-danger:hover{color:#d9534f;border-color:#d9534f;background:#fff}.form-control{height:32px}.input-label:before{display:inline-block;content:"*";color:#f44336;font-family:SimSun;font-size:12px;-webkit-transform:TranslateX(-10px);-ms-transform:TranslateX(-10px);transform:TranslateX(-10px)}.badge-main,.label.label-main{color:#fff;background-color:#1d9d74;border-color:#1d9d74}.bootstrap-tagsinput{background-color:#fff;border:1px solid #ccc;box-shadow:inset 0 1px 1px rgba(0,0,0,.075);display:inline-block;padding:4px 6px;color:#555;vertical-align:middle;border-radius:4px;width:85%;height:100px;line-height:20px;cursor:text}.bootstrap-tagsinput>.dropdown-menu{min-width:40px;font-size:12px}.bootstrap-tagsinput>.dropdown-menu>.active>a,.bootstrap-tagsinput>.dropdown-menu>.active>a:focus,.bootstrap-tagsinput>.dropdown-menu>.active>a:hover{background-image:-webkit-linear-gradient(top,#1d9d74 0,#1d9d74 100%);background-image:-o-linear-gradient(top,#1d9d74 0,#1d9d74 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#1d9d74),to(#1d9d74));background-image:linear-gradient(to bottom,#1d9d74 0,#1d9d74 100%);filter:progid: DXImageTransform.Microsoft.gradient(startColorstr='#1d9d74', endColorstr='#1d9d74', GradientType=0);background-repeat:repeat-x;color:#fff;text-decoration:none;outline:0;background-color:#1d9d74}.inputs-header{padding:9px 0;height:50px;border-top-left-radius:4px;border-top-right-radius:4px}.inputs-header .brand{font-size:13px;margin:2px 10px;font-weight:700;float:left}.inputs-header .brand>a{color:#666}.inputs-header>input{float:right;margin:1px 10px;height:30px;padding:0 10px;color:#666}.inputs-header>a{float:right;margin:1px 10px;height:30px;padding:5 5px}.inputs-header>select{float:right;max-width:80px;margin:1px 10px;padding:0 10px;color:#666;height:25px;font-size:12px}.witdh-150{max-width:150px}.witdh-200{max-width:200px}.card.highlight{border-color:#d9534f}.card .pagination-footer{height:40px;font-size:10px;color:#777;margin:-15px 20px 5px}.card .pagination-footer .tools{font-size:12px;margin:11px 20px 11px 0;float:right;display:inline}.card>.pagination-footer>.tools>span>input{height:25px;max-width:50px;display:inline}.pagination{display:inline-block;padding-left:0;margin:8px 0;float:right;border-radius:4px}.pagination>a{margin-right:5px;height:28px;width:28px;padding:5px 0}.datepicker>.table>tbody>tr>td,.datepicker>.table>thead>tr>td,.timepicker>.table>tbody>tr>td,.timepicker>.table>thead>tr>td{padding:5px 3px}.datepicker>.table>tbody>tr>td>.btn,.datepicker>.table>thead>tr>td>.btn,.timepicker>.table>tbody>tr>td>.btn,.timepicker>.table>thead>tr>td>.btn{border:1px solid #FFFDFD}.datepicker>.table>tbody>tr>td>.btn-default:active,.datepicker>.table>tbody>tr>td>.btn-default:focus,.datepicker>.table>tbody>tr>td>.btn-default:hover,.datepicker>.table>thead>tr>td>.btn-default:active,.datepicker>.table>thead>tr>td>.btn-default:focus,.datepicker>.table>thead>tr>td>.btn-default:hover,.timepicker>.table>tbody>tr>td>.btn-default:active,.timepicker>.table>tbody>tr>td>.btn-default:focus,.timepicker>.table>tbody>tr>td>.btn-default:hover,.timepicker>.table>thead>tr>td>.btn-default:active,.timepicker>.table>thead>tr>td>.btn-default:focus,.timepicker>.table>thead>tr>td>.btn-default:hover{color:#1d9d74;border-color:#1d9d74;background:#fff}.datepicker>.table>tbody>tr>td>a,.datepicker>.table>thead>tr>td>a,.timepicker>.table>tbody>tr>td>a,.timepicker>.table>thead>tr>td>a{height:25px;width:25px;padding:3px 0}.datepicker>.table>tbody>tr:first-child>td>a{padding:4px 0}.datepicker>.table>tbody>tr>td>a.btn.active,.datepicker>.table>thead>tr>td>a.btn.active,.timepicker>.table>tbody>tr>td>a.btn.active,.timepicker>.table>thead>tr>td>a.btn.active{color:#1d9d74;border-color:#1d9d74;background:#fff;box-shadow:inset 0 0 0 rgba(0,0,0,.125)}.datepicker>.table>thead>tr>td:not(:first-child):last-child>a,.timepicker>.table>thead>tr>td:not(:first-child):last-child>a{height:25px;width:50px;padding:5px 0}.datepicker>.table>tbody>tr>td>a,.timepicker>.table>tbody>tr>td>a{margin-left:8px}.sortorder:after{content:'\25b2'}.sortorder.reverse:after{content:'\25bc'}.input-control select{-moz-appearance:none;-webkit-appearance:none;appearance:none;position:relative;border:1px solid #d9d9d9;width:100%;height:100%;padding:.3125rem;z-index:0}.navbar-inverse{background-color:#337ab7;border-color:#337ab7}.sidebar{z-index:1;width:220px;top:0;left:0;height:100%}#page-wrapper{position:inherit;margin:70px 0 0 220px;padding:12px 30px;border-left:0 solid #e7e7e7}.sidebar .sidebar-nav.navbar-collapse{background-color:#F5F5F5;position:relative;color:#000;width:100%;padding:0;margin:0;list-style:none inside}.sidebar a{color:#555}.sidebar ul li:hover{color:red}.form-control{border-radius:8px}.form-control:focus,.highlight-border{border-color:#337ab7;box-shadow:0 0 0 rgba(0,0,0,.075) inset,0 0 0 rgba(29,157,116,1)}.btn-outline-primary.focus,.btn-outline-primary:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.browsehappy{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.btn.btn-main{color:#fff;background-color:#337ab7;border-color:#337ab7}.btn-default-inverse,.btn-default-inverse:focus,.btn-default-inverse:hover,.btn-default:active{color:#337ab7;border-color:#337ab7;background:#fff}.btn-danger-inverse,.btn-danger-inverse:focus,.btn-danger-inverse:hover,.btn-danger:active{color:#d9534f;border-color:#d9534f;background:#fff}.btn-tab-active,.btn-tab-active:focus,.btn-tab-active:hover,.btn-tab-default:active,.btn-tab-default:focus,.btn-tab-default:hover{color:#337ab7;border-color:#337ab7;background:#fff;font-weight:600}.btn-tab-default{color:#777;background:#fff;font-weight:600}.pagination>.btn.active{color:#fff;background-color:#337ab7;border-color:#337ab7}.btn-default:active,.btn-default:focus,.btn-default:hover{color:#337ab7;border-color:#337ab7;background:#fff}.bootstrap-switch.bootstrap-switch-on{border-color:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{color:#fff;background:#337ab7}.selectize-input-200>.selectize-input{min-width:200px;border-color:#337ab7}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-secondary.focus,.btn-outline-secondary:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-success.focus,.btn-outline-success:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-info.focus,.btn-outline-info:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-warning.focus,.btn-outline-warning:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-danger.focus,.btn-outline-danger:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-light.focus,.btn-outline-light:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-dark.focus,.btn-outline-dark:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/js/app.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/js/app.js new file mode 100644 index 0000000..ccab2f6 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/dist/js/app.js @@ -0,0 +1 @@ +"use strict";var app;angular.module("sentinelDashboardApp",["oc.lazyLoad","ui.router","ui.bootstrap","angular-loading-bar","ngDialog","ui.bootstrap.datetimepicker","ui-notification","rzTable","angular-clipboard","selectize","angularUtils.directives.dirPagination"]).factory("AuthInterceptor",["$window","$state",function(t,r){return{responseError:function(e){return 401===e.status&&(t.localStorage.removeItem("session_sentinel_admin"),r.go("login")),e},response:function(e){return e},request:function(e){return e},requestError:function(e){return e}}}]).config(["$stateProvider","$urlRouterProvider","$ocLazyLoadProvider","$httpProvider",function(e,t,r,a){a.interceptors.push("AuthInterceptor"),r.config({debug:!1,events:!0}),t.otherwise("/dashboard/home"),e.state("login",{url:"/login",templateUrl:"app/views/login.html",controller:"LoginCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/login.js"]})}]}}).state("dashboard",{url:"/dashboard",templateUrl:"app/views/dashboard/main.html",resolve:{loadMyDirectives:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/directives/header/header.js","app/scripts/directives/sidebar/sidebar.js","app/scripts/directives/sidebar/sidebar-search/sidebar-search.js"]})}]}}).state("dashboard.home",{url:"/home",templateUrl:"app/views/dashboard/home.html",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/main.js"]})}]}}).state("dashboard.flowV1",{templateUrl:"app/views/flow_v1.html",url:"/flow/:app",controller:"FlowControllerV1",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/flow_v1.js"]})}]}}).state("dashboard.flow",{templateUrl:"app/views/flow_v2.html",url:"/v2/flow/:app",controller:"FlowControllerV2",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/flow_v2.js"]})}]}}).state("dashboard.paramFlow",{templateUrl:"app/views/param_flow.html",url:"/paramFlow/:app",controller:"ParamFlowController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/param_flow.js"]})}]}}).state("dashboard.clusterAppAssignManage",{templateUrl:"app/views/cluster_app_assign_manage.html",url:"/cluster/assign_manage/:app",controller:"SentinelClusterAppAssignManageController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_assign_manage.js"]})}]}}).state("dashboard.clusterAppServerList",{templateUrl:"app/views/cluster_app_server_list.html",url:"/cluster/server/:app",controller:"SentinelClusterAppServerListController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_server_list.js"]})}]}}).state("dashboard.clusterAppClientList",{templateUrl:"app/views/cluster_app_client_list.html",url:"/cluster/client/:app",controller:"SentinelClusterAppTokenClientListController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_token_client_list.js"]})}]}}).state("dashboard.clusterSingle",{templateUrl:"app/views/cluster_single_config.html",url:"/cluster/single/:app",controller:"SentinelClusterSingleController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_single.js"]})}]}}).state("dashboard.authority",{templateUrl:"app/views/authority.html",url:"/authority/:app",controller:"AuthorityRuleController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/authority.js"]})}]}}).state("dashboard.degrade",{templateUrl:"app/views/degrade.html",url:"/degrade/:app",controller:"DegradeCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/degrade.js"]})}]}}).state("dashboard.system",{templateUrl:"app/views/system.html",url:"/system/:app",controller:"SystemCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/system.js"]})}]}}).state("dashboard.machine",{templateUrl:"app/views/machine.html",url:"/app/:app",controller:"MachineCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/machine.js"]})}]}}).state("dashboard.identity",{templateUrl:"app/views/identity.html",url:"/identity/:app",controller:"IdentityCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/identity.js"]})}]}}).state("dashboard.gatewayIdentity",{templateUrl:"app/views/gateway/identity.html",url:"/gateway/identity/:app",controller:"GatewayIdentityCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/gateway/identity.js"]})}]}}).state("dashboard.metric",{templateUrl:"app/views/metric.html",url:"/metric/:app",controller:"MetricCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/metric.js"]})}]}}).state("dashboard.gatewayApi",{templateUrl:"app/views/gateway/api.html",url:"/gateway/api/:app",controller:"GatewayApiCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/gateway/api.js"]})}]}}).state("dashboard.gatewayFlow",{templateUrl:"app/views/gateway/flow.html",url:"/gateway/flow/:app",controller:"GatewayFlowCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/gateway/flow.js"]})}]}})}]),(app=angular.module("sentinelDashboardApp")).filter("range",[function(){return function(e,t){if(isNaN(t)||t<=0)return[];e=[];for(var r=1;r<=t;r++)e.push(r);return e}}]),(app=angular.module("sentinelDashboardApp")).service("VersionService",["$http",function(e){this.version=function(){return e({url:"/version",method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("AuthService",["$http",function(t){this.check=function(){return t({url:"/auth/check",method:"POST"})},this.login=function(e){return t({url:"/auth/login",params:e,method:"POST"})},this.logout=function(){return t({url:"/auth/logout",method:"POST"})}}]),(app=angular.module("sentinelDashboardApp")).service("AppService",["$http",function(e){this.getApps=function(){return e({url:"app/briefinfos.json",method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("FlowServiceV1",["$http",function(a){function t(e){return void 0===e||""===e||isNaN(e)||e<=0}this.queryMachineRules=function(e,t,r){return a({url:"/v1/flow/rules",params:{app:e,ip:t,port:r},method:"GET"})},this.newRule=function(e){e.resource,e.limitApp,e.grade,e.count,e.strategy,e.refResource,e.controlBehavior,e.warmUpPeriodSec,e.maxQueueingTimeMs,e.app,e.ip,e.port;return a({url:"/v1/flow/rule",data:e,method:"POST"})},this.saveRule=function(e){var t={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,strategy:e.strategy,refResource:e.refResource,controlBehavior:e.controlBehavior,warmUpPeriodSec:e.warmUpPeriodSec,maxQueueingTimeMs:e.maxQueueingTimeMs};return a({url:"/v1/flow/save.json",params:t,method:"PUT"})},this.deleteRule=function(e){var t={id:e.id,app:e.app};return a({url:"/v1/flow/delete.json",params:t,method:"DELETE"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.count||e.count<0?(alert("限流阈值必须大于等于 0"),!1):void 0===e.strategy||e.strategy<0?(alert("无效的流控模式"),!1):1!=e.strategy&&2!=e.strategy||void 0!==e.refResource&&""!=e.refResource?void 0===e.controlBehavior||e.controlBehavior<0?(alert("无效的流控整形方式"),!1):1==e.controlBehavior&&t(e.warmUpPeriodSec)?(alert("预热时长必须大于 0"),!1):2==e.controlBehavior&&t(e.maxQueueingTimeMs)?(alert("排队超时时间必须大于 0"),!1):!e.clusterMode||void 0!==e.clusterConfig&&void 0!==e.clusterConfig.thresholdType||(alert("集群限流配置不正确"),!1):(alert("请填写关联资源或入口"),!1)}}]),(app=angular.module("sentinelDashboardApp")).service("FlowServiceV2",["$http",function(a){function t(e){return void 0===e||""===e||isNaN(e)||e<=0}this.queryMachineRules=function(e,t,r){return a({url:"/v2/flow/rules",params:{app:e,ip:t,port:r},method:"GET"})},this.newRule=function(e){return a({url:"/v2/flow/rule",data:e,method:"POST"})},this.saveRule=function(e){return a({url:"/v2/flow/rule/"+e.id,data:e,method:"PUT"})},this.deleteRule=function(e){return a({url:"/v2/flow/rule/"+e.id,method:"DELETE"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.count||e.count<0?(alert("限流阈值必须大于等于 0"),!1):void 0===e.strategy||e.strategy<0?(alert("无效的流控模式"),!1):1!=e.strategy&&2!=e.strategy||void 0!==e.refResource&&""!=e.refResource?void 0===e.controlBehavior||e.controlBehavior<0?(alert("无效的流控整形方式"),!1):1==e.controlBehavior&&t(e.warmUpPeriodSec)?(alert("预热时长必须大于 0"),!1):2==e.controlBehavior&&t(e.maxQueueingTimeMs)?(alert("排队超时时间必须大于 0"),!1):!e.clusterMode||void 0!==e.clusterConfig&&void 0!==e.clusterConfig.thresholdType||(alert("集群限流配置不正确"),!1):(alert("请填写关联资源或入口"),!1)}}]),(app=angular.module("sentinelDashboardApp")).service("DegradeService",["$http",function(a){this.queryMachineRules=function(e,t,r){return a({url:"degrade/rules.json",params:{app:e,ip:t,port:r},method:"GET"})},this.newRule=function(e){return a({url:"/degrade/rule",data:e,method:"POST"})},this.saveRule=function(e){var t={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,timeWindow:e.timeWindow,statIntervalMs:e.statIntervalMs,minRequestAmount:e.minRequestAmount,slowRatioThreshold:e.slowRatioThreshold};return a({url:"/degrade/rule/"+e.id,data:t,method:"PUT"})},this.deleteRule=function(e){return a({url:"/degrade/rule/"+e.id,method:"DELETE"})},this.checkRuleValid=function(e){if(void 0===e.resource||""===e.resource)return alert("资源名称不能为空"),!1;if(void 0===e.grade||e.grade<0)return alert("未知的降级策略"),!1;if(void 0===e.count||""===e.count||e.count<0)return alert("降级阈值不能为空或小于 0"),!1;if(null==e.timeWindow||""===e.timeWindow||e.timeWindow<=0)return alert("熔断时长必须大于 0s"),!1;if(null==e.minRequestAmount||e.minRequestAmount<=0)return alert("最小请求数目需大于 0"),!1;if(null==e.statIntervalMs||e.statIntervalMs<=0)return alert("统计窗口时长需大于 0s"),!1;if(void 0!==e.statIntervalMs&&12e4=r?n.apply(null,t):function(){return e(t.concat([].slice.apply(arguments)))}}(e)}function n(){var n=arguments,r=n.length-1;return function(){for(var e=r,t=n[r].apply(this,arguments);e--;)t=n[e].call(this,t);return t}}function l(){for(var e=[],t=0;tthis._limit&&this.evict(),e},e.prototype.evict=function(){var t=this._items.shift();return this._evictListeners.forEach(function(e){return e(t)}),t},e.prototype.dequeue=function(){if(this.size())return this._items.splice(0,1)[0]},e.prototype.clear=function(){var e=this._items;return this._items=[],e},e.prototype.size=function(){return this._items.length},e.prototype.remove=function(e){var t=this._items.indexOf(e);return-1 "+Ue(e))},e.prototype.traceTransitionIgnored=function(e){this.enabled(g.Category.TRANSITION)&&console.log(st(e)+": Ignored <> "+Ue(e))},e.prototype.traceHookInvocation=function(e,t,n){if(this.enabled(g.Category.HOOK)){var r=S("traceData.hookType")(n)||"internal",i=S("traceData.context.state.name")(n)||S("traceData.context")(n)||"unknown",o=He(e.registeredHook.callback);console.log(st(t)+": Hook -> "+r+" context: "+i+", "+Fe(200,o))}},e.prototype.traceHookResult=function(e,t,n){this.enabled(g.Category.HOOK)&&console.log(st(t)+": <- Hook returned: "+Fe(200,Ue(e)))},e.prototype.traceResolvePath=function(e,t,n){this.enabled(g.Category.RESOLVE)&&console.log(st(n)+": Resolving "+e+" ("+t+")")},e.prototype.traceResolvableResolved=function(e,t){this.enabled(g.Category.RESOLVE)&&console.log(st(t)+": <- Resolved "+e+" to: "+Fe(200,Ue(e.data)))},e.prototype.traceError=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Rejected "+Ue(t)+", reason: "+e)},e.prototype.traceSuccess=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Success "+Ue(t)+", final state: "+e.name)},e.prototype.traceUIViewEvent=function(e,t,n){void 0===n&&(n=""),this.enabled(g.Category.UIVIEW)&&console.log("ui-view: "+Le(30,e)+" "+et(t)+n)},e.prototype.traceUIViewConfigUpdated=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Updating",e," with ViewConfig from context='"+t+"'")},e.prototype.traceUIViewFill=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Fill",e," with: "+Fe(200,t))},e.prototype.traceViewSync=function(e){if(this.enabled(g.Category.VIEWCONFIG)){var a="uiview component fqn",t=e.map(function(e){var t,n=e.uiView,r=e.viewConfig,i=n&&n.fqn,o=r&&r.viewDecl.$context.name+": ("+r.viewDecl.$name+")";return(t={})[a]=i,t["view config state (view name)"]=o,t}).sort(function(e,t){return(e[a]||"").localeCompare(t[a]||"")});it(t)}},e.prototype.traceViewServiceEvent=function(e,t){var n,r,i;this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+(r=(n=t).viewDecl,i=r.$context.name||"(root)","[View#"+n.$id+" from '"+i+"' state]: target ui-view: '"+r.$uiViewName+"@"+r.$uiViewContextAnchor+"'"))},e.prototype.traceViewServiceUIViewEvent=function(e,t){this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+et(t))},e}(),ut=new lt,ct=function(){function e(e){this.pattern=/.*/,this.inherit=!0,N(this,e)}return e.prototype.is=function(e,t){return!0},e.prototype.encode=function(e,t){return e},e.prototype.decode=function(e,t){return e},e.prototype.equals=function(e,t){return e==t},e.prototype.$subPattern=function(){var e=this.pattern.toString();return e.substr(1,e.length-2)},e.prototype.toString=function(){return"{ParamType:"+this.name+"}"},e.prototype.$normalize=function(e){return this.is(e)?e:this.decode(e)},e.prototype.$asArray=function(e,t){if(!e)return this;if("auto"===e&&!t)throw new Error("'auto' array mode is for query parameters only");return new dt(this,e)},e}();function dt(r,i){var o=this;function a(e){return E(e)?e:k(e)?[e]:[]}function s(n,r){return function(e){if(E(e)&&0===e.length)return e;var t=ce(a(e),n);return!0===r?0===se(t,function(e){return!e}).length:function(e){switch(e.length){case 0:return;case 1:return"auto"===i?e[0]:e;default:return e}}(t)}}function l(o){return function(e,t){var n=a(e),r=a(t);if(n.length!==r.length)return!1;for(var i=0;i=n.invokeLimit&&n.deregister()}}},o.prototype.handleHookResult=function(e){var t=this,n=this.getNotCurrentRejection();return n||(R(e)?e.then(function(e){return t.handleHookResult(e)}):(ut.traceHookResult(e,this.transition,this.options),!1===e?Ve.aborted("Hook aborted transition").toPromise():h($t)(e)?Ve.redirected(e).toPromise():void 0))},o.prototype.getNotCurrentRejection=function(){var e=this.transition.router;return e._disposed?Ve.aborted("UIRouter instance #"+e.$id+" has been stopped (disposed)").toPromise():this.transition._aborted?Ve.aborted().toPromise():this.isSuperseded()?Ve.superseded(this.options.current()).toPromise():void 0},o.prototype.toString=function(){var e=this.options,t=this.registeredHook;return(S("traceData.hookType")(e)||"internal")+" context: "+(S("traceData.context.state.name")(e)||S("traceData.context")(e)||"unknown")+", "+Fe(200,Ye(t.callback))},o.HANDLE_RESULT=function(t){return function(e){return t.handleHookResult(e)}},o.LOG_REJECTED_RESULT=function(t){return function(e){R(e)&&e.catch(function(e){return t.logError(Ve.normalize(e))})}},o.LOG_ERROR=function(t){return function(e){return t.logError(e)}},o.REJECT_ERROR=function(e){return function(e){return Pe(e)}},o.THROW_ERROR=function(e){return function(e){throw e}},o}();function Gt(e,t){var i=O(t)?[t]:t;return!!(D(i)?i:function(e){for(var t=i,n=0;n "+(this.valid()?"":"(X) ")+"'"+(T(t)?t.name:t)+"'"+Ue(n(this.params()))+" )"},t.diToken=t}();function en(e,t){var n=["",""],r=e.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!t)return r;switch(t.squash){case!1:n=["(",")"+(t.isOptional?"?":"")];break;case!0:r=r.replace(/\/$/,""),n=["(?:/(",")|/)?"];break;default:n=["("+t.squash+"|",")?"]}return r+n[0]+t.type.pattern.source+n[1]}var tn=Xe("/"),nn={state:{params:{}},strict:!0,caseInsensitive:!0},rn=function(){function m(o,a,e,t){var s=this;this._cache={path:[this]},this._children=[],this._params=[],this._segments=[],this._compiled=[],this.config=t=te(t,nn),this.pattern=o;for(var n,r,i,l=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,u=/([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,c=[],d=0,p=function(e){if(!m.nameValidator.test(e))throw new Error("Invalid parameter name '"+e+"' in pattern '"+o+"'");if(le(s._params,v("id",e)))throw new Error("Duplicate parameter name '"+e+"' in pattern '"+o+"'")},h=function(e,t){var n,r=e[2]||e[3],i=t?e[4]:e[4]||("*"===e[1]?"[\\s\\S]*":null);return{id:r,regexp:i,segment:o.substring(d,e.index),type:i?a.type(i)||(n=i,W(a.type(t?"query":"path"),{pattern:new RegExp(n,s.config.caseInsensitive?"i":void 0)})):null}};(n=l.exec(o))&&!(0<=(r=h(n,!1)).segment.indexOf("?"));)p(r.id),this._params.push(e.fromPath(r.id,r.type,t.state)),this._segments.push(r.segment),c.push([r.segment,De(this._params)]),d=l.lastIndex;var f=(i=o.substring(d)).indexOf("?");if(0<=f){var g=i.substring(f);if(i=i.substring(0,f),0r.weight?s:r}return r},t.prototype.sync=function(e){if(!e||!e.defaultPrevented){var t=this._router,n=t.urlService,r=t.stateService,i={path:n.path(),search:n.search(),hash:n.hash()},o=this.match(i);m([[O,function(e){return n.url(e,!0)}],[$t.isDef,function(e){return r.go(e.state,e.params,e.options)}],[h($t),function(e){return r.go(e.state(),e.params(),e.options())}]])(o&&o.rule.handler(o.match,i,t))}},t.prototype.listen=function(e){var t=this;if(!1!==e)return this._stopFn=this._stopFn||this._router.urlService.onChange(function(e){return t.sync(e)});this._stopFn&&this._stopFn(),delete this._stopFn},t.prototype.update=function(e){var t=this._router.locationService;e?this.location=t.url():t.url()!==this.location&&t.url(this.location,!0)},t.prototype.push=function(e,t,n){var r=n&&!!n.replace;this._router.urlService.url(e.format(t||{}),r)},t.prototype.href=function(e,t,n){var r=e.format(t);if(null==r)return null;n=n||{absolute:!1};var i,o,a,s,l=this._router.urlService.config,u=l.html5Mode();if(u||null===r||(r="#"+l.hashPrefix()+r),i=r,o=u,a=n.absolute,r="/"===(s=l.baseHref())?i:o?We(s)+i:a?s.slice(1)+i:i,!n.absolute||!r)return r;var c=!u&&r?"/":"",d=l.port(),p=80===d||443===d?"":":"+d;return[l.protocol(),"://",l.host(),p,c,r].join("")},t.prototype.rule=function(e){var t=this;if(!ln.isUrlRule(e))throw new Error("invalid rule");return e.$id=this._id++,e.priority=e.priority||0,this._rules.push(e),this._sorted=!1,function(){return t.removeRule(e)}},t.prototype.removeRule=function(e){Q(this._rules,e)},t.prototype.rules=function(){return this.ensureSorted(),this._rules.slice()},t.prototype.otherwise=function(e){var t=pn(e);this._otherwiseFn=this.urlRuleFactory.create(f(!0),t),this._sorted=!1},t.prototype.initial=function(e){var t=pn(e);this.rule(this.urlRuleFactory.create(function(e,t){return 0===t.globals.transitionHistory.size()&&!!/^\/?$/.exec(e.path)},t))},t.prototype.when=function(e,t,n){var r=this.urlRuleFactory.create(e,t);return k(n&&n.priority)&&(r.priority=n.priority),this.rule(r),r},t.prototype.deferIntercept=function(e){void 0===e&&(e=!0),this.interceptDeferred=e},t}();function pn(e){if(!(D(e)||O(e)||h($t)(e)||$t.isDef(e)))throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property");return D(e)?e:f(e)}var hn=function(){function l(e){var n=this;this.router=e,this._uiViews=[],this._viewConfigs=[],this._viewConfigFactories={},this._listeners=[],this._pluginapi={_rootViewContext:this._rootViewContext.bind(this),_viewConfigFactory:this._viewConfigFactory.bind(this),_registeredUIView:function(t){return le(n._uiViews,function(e){return n.router.$id+"."+e.id===t})},_registeredUIViews:function(){return n._uiViews},_activeViewConfigs:function(){return n._viewConfigs},_onSync:function(e){return n._listeners.push(e),function(){return Q(n._listeners,e)}}}}return l.normalizeUIViewTarget=function(e,t){void 0===t&&(t="");var n=t.split("@"),r=n[0]||"$default",i=O(n[1])?n[1]:"^",o=/^(\^(?:\.\^)*)\.(.*$)/.exec(r);o&&(i=o[1],r=o[2]),"!"===r.charAt(0)&&(r=r.substr(1),i="");/^(\^(?:\.\^)*)$/.exec(i)?i=i.split(".").reduce(function(e,t){return e.parent},e).name:"."===i&&(i=e.name);return{uiViewName:r,uiViewContextAnchor:i}},l.prototype._rootViewContext=function(e){return this._rootContext=e||this._rootContext},l.prototype._viewConfigFactory=function(e,t){this._viewConfigFactories[e]=t},l.prototype.createViewConfig=function(e,t){var n=this._viewConfigFactories[t.$type];if(!n)throw new Error("ViewService: No view config factory registered for type "+t.$type);var r=n(e,t);return E(r)?r:[r]},l.prototype.deactivateViewConfig=function(e){ut.traceViewServiceEvent("<- Removing",e),Q(this._viewConfigs,e)},l.prototype.activateViewConfig=function(e){ut.traceViewServiceEvent("-> Registering",e),this._viewConfigs.push(e)},l.prototype.sync=function(){var n=this,r=this._uiViews.map(function(e){return[e.fqn,e]}).reduce(ke,{});function i(e){for(var t=e.viewDecl.$context,n=0;++n&&t.parent;)t=t.parent;return n}var o=u(function(e,t,n,r){return t*(e(n)-e(r))}),e=this._uiViews.sort(o(function(e){var t=function(e){return e&&e.parent?t(e.parent)+1:1};return 1e4*e.fqn.split(".").length+t(e.creationContext)},1)).map(function(e){var t=n._viewConfigs.filter(l.matches(r,e));return 1 Registering",t);var e=this._uiViews;return e.filter(function(e){return e.fqn===t.fqn&&e.$type===t.$type}).length&&ut.traceViewServiceUIViewEvent("!!!! duplicate uiView named:",t),e.push(t),this.sync(),function(){-1!==e.indexOf(t)?(ut.traceViewServiceUIViewEvent("<- Deregistering",t),Q(e)(t)):ut.traceViewServiceUIViewEvent("Tried removing non-registered uiView",t)}},l.prototype.available=function(){return this._uiViews.map(w("fqn"))},l.prototype.active=function(){return this._uiViews.filter(w("$config")).map(w("name"))},l.matches=function(s,l){return function(e){if(l.$type!==e.viewDecl.$type)return!1;var t=e.viewDecl,n=t.$uiViewName.split("."),r=l.fqn.split(".");if(!q(n,r.slice(0-n.length)))return!1;var i=1-n.length||void 0,o=r.slice(0,i).join("."),a=s[o].creationContext;return t.$uiViewContextAnchor===(a&&a.name)}},l}(),fn=function(){function e(){this.params=new wt,this.lastStartedTransitionId=-1,this.transitionHistory=new Re([],1),this.successfulTransitions=new Re([],1)}return e.prototype.dispose=function(){this.transitionHistory.clear(),this.successfulTransitions.clear(),this.transition=null},e}(),gn=function(e){return e.reduce(function(e,t){return e[t]=I(t),e},{dispose:z})},mn=["url","path","search","hash","onChange"],vn=["port","protocol","host","baseHref","html5Mode","hashPrefix"],yn=["type","caseInsensitive","strictMode","defaultSquashPolicy"],wn=["sort","when","initial","otherwise","rules","rule","removeRule"],bn=["deferIntercept","listen","sync","match"],$n=function(){function e(e,t){void 0===t&&(t=!0),this.router=e,this.rules={},this.config={};var n=function(){return e.locationService};B(n,this,n,mn,t);var r=function(){return e.locationConfig};B(r,this.config,r,vn,t);var i=function(){return e.urlMatcherFactory};B(i,this.config,i,yn);var o=function(){return e.urlRouter};B(o,this.rules,o,wn),B(o,this,o,bn)}return e.prototype.url=function(e,t,n){},e.prototype.path=function(){},e.prototype.search=function(){},e.prototype.hash=function(){},e.prototype.onChange=function(e){},e.prototype.parts=function(){return{path:this.path(),search:this.search(),hash:this.hash()}},e.prototype.dispose=function(){},e.prototype.sync=function(e){},e.prototype.listen=function(e){},e.prototype.deferIntercept=function(e){},e.prototype.match=function(e){},e.locationServiceStub=gn(mn),e.locationConfigStub=gn(vn),e}(),_n=0,Cn=function(){function e(e,t){void 0===e&&(e=$n.locationServiceStub),void 0===t&&(t=$n.locationConfigStub),this.locationService=e,this.locationConfig=t,this.$id=_n++,this._disposed=!1,this._disposables=[],this.trace=ut,this.viewService=new hn(this),this.globals=new fn,this.transitionService=new zn(this),this.urlMatcherFactory=new sn,this.urlRouter=new dn(this),this.stateRegistry=new zt(this),this.stateService=new Bn(this),this.urlService=new $n(this),this._plugins={},this.viewService._pluginapi._rootViewContext(this.stateRegistry.root()),this.globals.$current=this.stateRegistry.root(),this.globals.current=this.globals.$current.self,this.disposable(this.globals),this.disposable(this.stateService),this.disposable(this.stateRegistry),this.disposable(this.transitionService),this.disposable(this.urlRouter),this.disposable(e),this.disposable(t)}return e.prototype.disposable=function(e){this._disposables.push(e)},e.prototype.dispose=function(e){var t=this;e&&D(e.dispose)?e.dispose(this):(this._disposed=!0,this._disposables.slice().forEach(function(e){try{"function"==typeof e.dispose&&e.dispose(t),Q(t._disposables,e)}catch(e){}}))},e.prototype.plugin=function(e,t){void 0===t&&(t={});var n=new e(this,t);if(!n.name)throw new Error("Required property `name` missing on plugin: "+n);return this._disposables.push(n),this._plugins[n.name]=n},e.prototype.getPlugin=function(e){return e?this._plugins[e]:de(this._plugins)},e}();function Sn(t){t.addResolvable(kt.fromData(Cn,t.router),""),t.addResolvable(kt.fromData(Jt,t),""),t.addResolvable(kt.fromData("$transition$",t),""),t.addResolvable(kt.fromData("$stateParams",t.params()),""),t.entering().forEach(function(e){t.addResolvable(kt.fromData("$state$",e),e)})}var kn=G(["$transition$",Jt]),Dn=function(e){var t=de(e.treeChanges()).reduce(fe,[]).reduce(ve,[]),n=function(e){return kn(e.token)?kt.fromData(e.token,null):e};t.forEach(function(e){e.resolvables=e.resolvables.map(n)})},xn=function(t){var e=t.to().redirectTo;if(e){var n=t.router.stateService;return D(e)?V.$q.when(e(t)).then(r):r(e)}function r(e){if(e)return e instanceof $t?e:O(e)?n.target(e,t.params(),t.options()):e.state||e.params?n.target(e.state||t.to(),e.params||t.params(),t.options()):void 0}};function On(n){return function(e,t){return(0,t.$$state()[n])(e,t)}}var Tn=On("onExit"),En=On("onRetain"),An=On("onEnter"),Pn=function(e){return new Et(e.treeChanges().to).resolvePath("EAGER",e).then(z)},Mn=function(e,t){return new Et(e.treeChanges().to).subContext(t.$$state()).resolvePath("LAZY",e).then(z)},Rn=function(e){return new Et(e.treeChanges().to).resolvePath("LAZY",e).then(z)},In=function(e){var t=V.$q,n=e.views("entering");if(n.length)return t.all(n.map(function(e){return t.when(e.load())})).then(z)},Vn=function(e){var t=e.views("entering"),n=e.views("exiting");if(t.length||n.length){var r=e.router.viewService;n.forEach(function(e){return r.deactivateViewConfig(e)}),t.forEach(function(e){return r.activateViewConfig(e)}),r.sync()}},Fn=function(e){var t=e.router.globals,n=function(){t.transition===e&&(t.transition=null)};e.onSuccess({},function(){t.successfulTransitions.enqueue(e),t.$current=e.$to(),t.current=t.$current.self,xe(e.params(),t.params)},{priority:1e4}),e.promise.then(n,n)},Ln=function(e){var t=e.options(),n=e.router.stateService,r=e.router.urlRouter;if("url"!==t.source&&t.location&&n.$current.navigable){var i={replace:"replace"===t.location};r.push(n.$current.navigable.url,n.params,i)}r.update(!0)},jn=function(a){var s=a.router;var e=a.entering().filter(function(e){return!!e.$$state().lazyLoad}).map(function(e){return Hn(a,e)});return V.$q.all(e).then(function(){if("url"!==a.originalTransition().options().source){var e=a.targetState();return s.stateService.target(e.identifier(),e.params(),e.options())}var t=s.urlService,n=t.match(t.parts()),r=n&&n.rule;if(r&&"STATE"===r.type){var i=r.state,o=n.match;return s.stateService.target(i,o,a.options())}s.urlService.sync()})};function Hn(t,n){var r=n.$$state().lazyLoad,e=r._promise;if(!e){e=r._promise=V.$q.when(r(t,n)).then(function(e){e&&Array.isArray(e.states)&&e.states.forEach(function(e){return t.router.stateRegistry.register(e)});return e}).then(function(e){return delete n.lazyLoad,delete n.$$state().lazyLoad,delete r._promise,e},function(e){return delete r._promise,V.$q.reject(e)})}return e}var Yn=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1),this.name=e,this.hookPhase=t,this.hookOrder=n,this.criteriaMatchPath=r,this.reverseSort=i,this.getResultHandler=o,this.getErrorHandler=a,this.synchronous=s};function Nn(e){var t=e._ignoredReason();if(t){ut.traceTransitionIgnored(e);var n=e.router.globals.transition;return"SameAsCurrent"===t&&n&&n.abort(),Ve.ignored().toPromise()}}function qn(e){if(!e.valid())throw new Error(e.error().toString())}var Un={location:!0,relative:null,inherit:!1,notify:!0,reload:!1,custom:{},current:function(){return null},source:"unknown"},zn=function(){function e(e){this._transitionCount=0,this._eventTypes=[],this._registeredHooks={},this._criteriaPaths={},this._router=e,this.$view=e.viewService,this._deregisterHookFns={},this._pluginapi=B(f(this),{},f(this),["_definePathType","_defineEvent","_getPathTypes","_getEvents","getHooks"]),this._defineCorePaths(),this._defineCoreEvents(),this._registerCoreTransitionHooks(),e.globals.successfulTransitions.onEvict(Dn)}return e.prototype.onCreate=function(e,t,n){},e.prototype.onBefore=function(e,t,n){},e.prototype.onStart=function(e,t,n){},e.prototype.onExit=function(e,t,n){},e.prototype.onRetain=function(e,t,n){},e.prototype.onEnter=function(e,t,n){},e.prototype.onFinish=function(e,t,n){},e.prototype.onSuccess=function(e,t,n){},e.prototype.onError=function(e,t,n){},e.prototype.dispose=function(e){de(this._registeredHooks).forEach(function(t){return t.forEach(function(e){e._deregistered=!0,Q(t,e)})})},e.prototype.create=function(e,t){return new Jt(e,t,this._router)},e.prototype._defineCoreEvents=function(){var e=g.TransitionHookPhase,t=Wt,n=this._criteriaPaths;this._defineEvent("onCreate",e.CREATE,0,n.to,!1,t.LOG_REJECTED_RESULT,t.THROW_ERROR,!0),this._defineEvent("onBefore",e.BEFORE,0,n.to),this._defineEvent("onStart",e.RUN,0,n.to),this._defineEvent("onExit",e.RUN,100,n.exiting,!0),this._defineEvent("onRetain",e.RUN,200,n.retained),this._defineEvent("onEnter",e.RUN,300,n.entering),this._defineEvent("onFinish",e.RUN,400,n.to),this._defineEvent("onSuccess",e.SUCCESS,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0),this._defineEvent("onError",e.ERROR,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0)},e.prototype._defineCorePaths=function(){var e=g.TransitionHookScope.STATE,t=g.TransitionHookScope.TRANSITION;this._definePathType("to",t),this._definePathType("from",t),this._definePathType("exiting",e),this._definePathType("retained",e),this._definePathType("entering",e)},e.prototype._defineEvent=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1);var l=new Yn(e,t,n,r,i,o,a,s);this._eventTypes.push(l),Qt(this,this,l)},e.prototype._getEvents=function(t){return(k(t)?this._eventTypes.filter(function(e){return e.hookPhase===t}):this._eventTypes.slice()).sort(function(e,t){var n=e.hookPhase-t.hookPhase;return 0===n?e.hookOrder-t.hookOrder:n})},e.prototype._definePathType=function(e,t){this._criteriaPaths[e]={name:e,scope:t}},e.prototype._getPathTypes=function(){return this._criteriaPaths},e.prototype.getHooks=function(e){return this._registeredHooks[e]},e.prototype._registerCoreTransitionHooks=function(){var e=this._deregisterHookFns;e.addCoreResolves=this.onCreate({},Sn),e.ignored=this.onBefore({},Nn,{priority:-9999}),e.invalid=this.onBefore({},qn,{priority:-1e4}),e.redirectTo=this.onStart({to:function(e){return!!e.redirectTo}},xn),e.onExit=this.onExit({exiting:function(e){return!!e.onExit}},Tn),e.onRetain=this.onRetain({retained:function(e){return!!e.onRetain}},En),e.onEnter=this.onEnter({entering:function(e){return!!e.onEnter}},An),e.eagerResolve=this.onStart({},Pn,{priority:1e3}),e.lazyResolve=this.onEnter({entering:f(!0)},Mn,{priority:1e3}),e.resolveAll=this.onFinish({},Rn,{priority:1e3}),e.loadViews=this.onFinish({},In),e.activateViews=this.onSuccess({},Vn),e.updateGlobals=this.onCreate({},Fn),e.updateUrl=this.onSuccess({},Ln,{priority:9999}),e.lazyLoad=this.onBefore({entering:function(e){return!!e.lazyLoad}},jn)},e}(),Bn=function(){function n(e){this.router=e,this.invalidCallbacks=[],this._defaultErrorHandler=function(e){e instanceof Error&&e.stack?(console.error(e),console.error(e.stack)):e instanceof Ve?(console.error(e.toString()),e.detail&&e.detail.stack&&console.error(e.detail.stack)):console.error(e)};var t=Object.keys(n.prototype).filter(d(G(["current","$current","params","transition"])));B(f(n.prototype),this,f(this),t)}return Object.defineProperty(n.prototype,"transition",{get:function(){return this.router.globals.transition},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"params",{get:function(){return this.router.globals.params},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"current",{get:function(){return this.router.globals.current},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"$current",{get:function(){return this.router.globals.$current},enumerable:!0,configurable:!0}),n.prototype.dispose=function(){this.defaultErrorHandler(z),this.invalidCallbacks=[]},n.prototype._handleInvalidTargetState=function(e,n){var r=this,i=_t.makeTargetState(this.router.stateRegistry,e),t=this.router.globals,o=function(){return t.transitionHistory.peekTail()},a=o(),s=new Re(this.invalidCallbacks.slice()),l=new Et(e).injector(),u=function(e){if(e instanceof $t){var t=e;return(t=r.target(t.identifier(),t.params(),t.options())).valid()?o()!==a?Ve.superseded().toPromise():r.transitionTo(t.identifier(),t.params(),t.options()):Ve.invalid(t.error()).toPromise()}};return function t(){var e=s.dequeue();return void 0===e?Ve.invalid(n.error()).toPromise():V.$q.when(e(n,i,l)).then(u).then(function(e){return e||t()})}()},n.prototype.onInvalid=function(e){return this.invalidCallbacks.push(e),function(){Q(this.invalidCallbacks)(e)}.bind(this)},n.prototype.reload=function(e){return this.transitionTo(this.current,this.params,{reload:!k(e)||e,inherit:!1,notify:!1})},n.prototype.go=function(e,t,n){var r=te(n,{relative:this.$current,inherit:!0},Un);return this.transitionTo(e,t,r)},n.prototype.target=function(e,t,n){if(void 0===n&&(n={}),T(n.reload)&&!n.reload.name)throw new Error("Invalid reload state object");var r=this.router.stateRegistry;if(n.reloadState=!0===n.reload?r.root():r.matcher.find(n.reload,n.relative),n.reload&&!n.reloadState)throw new Error("No such reload state '"+(O(n.reload)?n.reload:n.reload.name)+"'");return new $t(this.router.stateRegistry,e,t,n)},n.prototype.getCurrentPath=function(){var e=this,t=this.router.globals.successfulTransitions.peekTail();return t?t.treeChanges().to:[new bt(e.router.stateRegistry.root())]},n.prototype.transitionTo=function(e,t,n){var o=this;void 0===t&&(t={}),void 0===n&&(n={});var a=this.router,s=a.globals;n=te(n,Un);n=N(n,{current:function(){return s.transition}});var r=this.target(e,t,n),i=this.getCurrentPath();if(!r.exists())return this._handleInvalidTargetState(i,r);if(!r.valid())return Pe(r.error());var l=function(i){return function(e){if(e instanceof Ve){var t=a.globals.lastStartedTransitionId===i.$id;if(e.type===g.RejectType.IGNORED)return t&&a.urlRouter.update(),V.$q.when(s.current);var n=e.detail;if(e.type===g.RejectType.SUPERSEDED&&e.redirected&&n instanceof $t){var r=i.redirect(n);return r.run().catch(l(r))}if(e.type===g.RejectType.ABORTED)return t&&a.urlRouter.update(),V.$q.reject(e)}return o.defaultErrorHandler()(e),V.$q.reject(e)}},u=this.router.transitionService.create(i,r),c=u.run().catch(l(u));return Ae(c),N(c,{transition:u})},n.prototype.is=function(e,t,n){n=te(n,{relative:this.$current});var r=this.router.stateRegistry.matcher.find(e,n.relative);if(k(r)){if(this.$current!==r)return!1;if(!t)return!0;var i=r.parameters({inherit:!0,matchingKeys:t});return vt.equals(i,vt.values(i,t),this.params)}},n.prototype.includes=function(e,t,n){n=te(n,{relative:this.$current});var r=O(e)&&Me.fromString(e);if(r){if(!r.matches(this.$current.name))return!1;e=this.$current.name}var i=this.router.stateRegistry.matcher.find(e,n.relative),o=this.$current.includes;if(k(i)){if(!k(o[i.name]))return!1;if(!t)return!0;var a=i.parameters({inherit:!0,matchingKeys:t});return vt.equals(a,vt.values(a,t),this.params)}},n.prototype.href=function(e,t,n){n=te(n,{lossy:!0,inherit:!0,absolute:!1,relative:this.$current}),t=t||{};var r=this.router.stateRegistry.matcher.find(e,n.relative);if(!k(r))return null;n.inherit&&(t=this.params.$inherit(t,this.$current,r));var i=r&&n.lossy?r.navigable:r;return i&&void 0!==i.url&&null!==i.url?this.router.urlRouter.href(i.url,t,{absolute:n.absolute}):null},n.prototype.defaultErrorHandler=function(e){return this._defaultErrorHandler=e||this._defaultErrorHandler},n.prototype.get=function(e,t){var n=this.router.stateRegistry;return 0===arguments.length?n.get():n.get(e,t||this.$current)},n.prototype.lazyLoad=function(e,t){var n=this.get(e);if(!n||!n.lazyLoad)throw new Error("Can not lazy load "+e);var r=this.getCurrentPath(),i=_t.makeTargetState(this.router.stateRegistry,r);return Hn(t=t||this.router.transitionService.create(r,i),n)},n}(),Wn={when:function(n){return new Promise(function(e,t){return e(n)})},reject:function(n){return new Promise(function(e,t){t(n)})},defer:function(){var n={};return n.promise=new Promise(function(e,t){n.resolve=e,n.reject=t}),n},all:function(e){if(E(e))return Promise.all(e);if(T(e)){var t=Object.keys(e).map(function(t){return e[t].then(function(e){return{key:t,val:e}})});return Wn.all(t).then(function(e){return e.reduce(function(e,t){return e[t.key]=t.val,e},{})})}}},Gn={},Kn=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm,Qn=/([^\s,]+)/g,Zn={get:function(e){return Gn[e]},has:function(e){return null!=Zn.get(e)},invoke:function(e,t,n){var r=N({},Gn,n||{}),i=Zn.annotate(e),o=be(function(e){return r.hasOwnProperty(e)},function(e){return"DI can't find injectable: '"+e+"'"}),a=i.filter(o).map(function(e){return r[e]});return D(e)?e.apply(t,a):e.slice(-1)[0].apply(t,a)},annotate:function(e){if(!M(e))throw new Error("Not an injectable function: "+e);if(e&&e.$inject)return e.$inject;if(E(e))return e.slice(0,-1);var t=e.toString().replace(Kn,"");return t.slice(t.indexOf("(")+1,t.indexOf(")")).match(Qn)||[]}},Xn=function(e,t){var n=t[0],r=t[1];return e.hasOwnProperty(n)?E(e[n])?e[n].push(r):e[n]=[e[n],r]:e[n]=r,e},Jn=function(e){return e.split("&").filter(U).map(Qe).reduce(Xn,{})};function er(e){var t=function(e){return e||""},n=Ge(e).map(t),r=n[0],i=n[1],o=Ke(r).map(t);return{path:o[0],search:o[1],hash:i,url:e}}var tr=function(e){var t=e.path(),n=e.search(),r=e.hash(),i=Object.keys(n).map(function(t){var e=n[t];return(E(e)?e:[e]).map(function(e){return t+"="+e})}).reduce(fe,[]).join("&");return t+(i?"?"+i:"")+(r?"#"+r:"")};function nr(r,i,o,a){return function(e){var t=e.locationService=new o(e),n=e.locationConfig=new a(e,i);return{name:r,service:t,configuration:n,dispose:function(e){e.dispose(t),e.dispose(n)}}}}var rr,ir,or,ar=function(){function e(e,t){var n=this;this.fireAfterUpdate=t,this._listeners=[],this._listener=function(t){return n._listeners.forEach(function(e){return e(t)})},this.hash=function(){return er(n._get()).hash},this.path=function(){return er(n._get()).path},this.search=function(){return Jn(er(n._get()).search)},this._location=F.location,this._history=F.history}return e.prototype.url=function(t,e){return void 0===e&&(e=!0),k(t)&&t!==this._get()&&(this._set(null,null,t,e),this.fireAfterUpdate&&this._listeners.forEach(function(e){return e({url:t})})),tr(this)},e.prototype.onChange=function(e){var t=this;return this._listeners.push(e),function(){return Q(t._listeners,e)}},e.prototype.dispose=function(e){ee(this._listeners)},e}(),sr=(rr=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}rr(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),lr=function(n){function e(e){var t=n.call(this,e,!1)||this;return F.addEventListener("hashchange",t._listener,!1),t}return sr(e,n),e.prototype._get=function(){return Ze(this._location.hash)},e.prototype._set=function(e,t,n,r){this._location.hash=n},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),F.removeEventListener("hashchange",this._listener)},e}(ar),ur=(ir=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}ir(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),cr=function(t){function e(e){return t.call(this,e,!0)||this}return ur(e,t),e.prototype._get=function(){return this._url},e.prototype._set=function(e,t,n,r){this._url=n},e}(ar),dr=(or=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}or(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),pr=function(n){function e(e){var t=n.call(this,e,!0)||this;return t._config=e.urlService.config,F.addEventListener("popstate",t._listener,!1),t}return dr(e,n),e.prototype._getBasePrefix=function(){return We(this._config.baseHref())},e.prototype._get=function(){var e=this._location,t=e.pathname,n=e.hash,r=e.search;r=Ke(r)[1],n=Ge(n)[1];var i=this._getBasePrefix(),o=t===this._config.baseHref(),a=t.substr(0,i.length)===i;return(t=o?"/":a?t.substring(i.length):t)+(r?"?"+r:"")+(n?"#"+n:"")},e.prototype._set=function(e,t,n,r){var i=this._getBasePrefix(),o=n&&"/"!==n[0]?"/":"",a=""===n||"/"===n?this._config.baseHref():i+o+n;r?this._history.replaceState(e,t,a):this._history.pushState(e,t,a)},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),F.removeEventListener("popstate",this._listener)},e}(ar),hr=function(){var t=this;this.dispose=z,this._baseHref="",this._port=80,this._protocol="http",this._host="localhost",this._hashPrefix="",this.port=function(){return t._port},this.protocol=function(){return t._protocol},this.host=function(){return t._host},this.baseHref=function(){return t._baseHref},this.html5Mode=function(){return!1},this.hashPrefix=function(e){return k(e)?t._hashPrefix=e:t._hashPrefix}},fr=function(){function e(e,t){void 0===t&&(t=!1),this._isHtml5=t,this._baseHref=void 0,this._hashPrefix=""}return e.prototype.port=function(){return location.port?Number(location.port):"https"===this.protocol()?443:80},e.prototype.protocol=function(){return location.protocol.replace(/:/g,"")},e.prototype.host=function(){return location.hostname},e.prototype.html5Mode=function(){return this._isHtml5},e.prototype.hashPrefix=function(e){return k(e)?this._hashPrefix=e:this._hashPrefix},e.prototype.baseHref=function(e){return k(e)&&(this._baseHref=e),b(this._baseHref)&&(this._baseHref=this.getBaseHref()),this._baseHref},e.prototype.getBaseHref=function(){var e=document.getElementsByTagName("base")[0];return e&&e.href?e.href.replace(/^(https?:)?\/\/[^/]*/,""):location.pathname||"/"},e.prototype.dispose=function(){},e}();function gr(e){return V.$injector=Zn,{name:"vanilla.services",$q:V.$q=Wn,$injector:Zn,dispose:function(){return null}}}var mr=nr("vanilla.hashBangLocation",!1,lr,fr),vr=nr("vanilla.pushStateLocation",!0,pr,fr),yr=nr("vanilla.memoryLocation",!1,cr,hr),wr=function(){function e(){}return e.prototype.dispose=function(e){},e}(),br=Object.freeze({root:F,fromJson:j,toJson:H,forEach:Y,extend:N,equals:q,identity:U,noop:z,createProxyFunctions:B,inherit:W,inArray:G,_inArray:K,removeFrom:Q,_removeFrom:Z,pushTo:X,_pushTo:J,deregAll:ee,defaults:te,mergeR:ne,ancestors:re,pick:ie,omit:oe,pluck:ae,filter:se,find:le,mapObj:ue,map:ce,values:de,allTrueR:pe,anyTrueR:he,unnestR:fe,flattenR:ge,pushR:me,uniqR:ve,unnest:ye,flatten:we,assertPredicate:be,assertMap:$e,assertFn:_e,pairs:Ce,arrayTuples:Se,applyPairs:ke,tail:De,copy:xe,_extend:Oe,silenceUncaughtInPromise:Ae,silentRejection:Pe,notImplemented:I,services:V,Glob:Me,curry:u,compose:n,pipe:l,prop:w,propEq:v,parse:S,not:d,and:r,or:i,all:c,any:p,is:h,eq:o,val:f,invoke:a,pattern:m,isUndefined:b,isDefined:k,isNull:$,isNullOrUndefined:_,isFunction:D,isNumber:x,isString:O,isObject:T,isArray:E,isDate:A,isRegExp:P,isInjectable:M,isPromise:R,Queue:Re,maxLength:Fe,padString:Le,kebobString:je,functionToString:He,fnToString:Ye,stringify:Ue,beforeAfterSubstr:ze,hostRegex:Be,stripLastPathElement:We,splitHash:Ge,splitQuery:Ke,splitEqual:Qe,trimHashVal:Ze,splitOnDelim:Xe,joinNeighborsR:Je,get Category(){return g.Category},Trace:lt,trace:ut,get DefType(){return g.DefType},Param:vt,ParamTypes:yt,StateParams:wt,ParamType:ct,PathNode:bt,PathUtils:_t,resolvePolicies:Ct,defaultResolvePolicy:St,Resolvable:kt,NATIVE_INJECTOR_TOKEN:Tt,ResolveContext:Et,resolvablesBuilder:Lt,StateBuilder:Yt,StateObject:Nt,StateMatcher:qt,StateQueueManager:Ut,StateRegistry:zt,StateService:Bn,TargetState:$t,get TransitionHookPhase(){return g.TransitionHookPhase},get TransitionHookScope(){return g.TransitionHookScope},HookBuilder:Zt,matchState:Gt,RegisteredHook:Kt,makeEvent:Qt,get RejectType(){return g.RejectType},Rejection:Ve,Transition:Jt,TransitionHook:Wt,TransitionEventType:Yn,defaultTransOpts:Un,TransitionService:zn,UrlMatcher:rn,ParamFactory:an,UrlMatcherFactory:sn,UrlRouter:dn,UrlRuleFactory:ln,BaseUrlRule:un,UrlService:$n,ViewService:hn,UIRouterGlobals:fn,UIRouter:Cn,$q:Wn,$injector:Zn,BaseLocationServices:ar,HashLocationService:lr,MemoryLocationService:cr,PushStateLocationService:pr,MemoryLocationConfig:hr,BrowserLocationConfig:fr,keyValsToObjectR:Xn,getParams:Jn,parseUrl:er,buildUrl:tr,locationPluginFactory:nr,servicesPlugin:gr,hashLocationPlugin:mr,pushStateLocationPlugin:vr,memoryLocationPlugin:yr,UIRouterPluginBase:wr});function $r(){var n=null;return function(e,t){return n=n||V.$injector.get("$templateFactory"),[new kr(e,t,n)]}}var _r=function(e,n){return e.reduce(function(e,t){return e||k(n[t])},!1)};function Cr(r){if(!r.parent)return{};var i=["component","bindings","componentProvider"],o=["templateProvider","templateUrl","template","notify","async"].concat(["controller","controllerProvider","controllerAs","resolveAs"]),e=i.concat(o);if(k(r.views)&&_r(e,r))throw new Error("State '"+r.name+"' has a 'views' object. It cannot also have \"view properties\" at the state level. Move the following properties into a view (in the 'views' object): "+e.filter(function(e){return k(r[e])}).join(", "));var a={},t=r.views||{$default:ie(r,e)};return Y(t,function(e,t){if(t=t||"$default",O(e)&&(e={component:e}),e=N({},e),_r(i,e)&&_r(o,e))throw new Error("Cannot combine: "+i.join("|")+" with: "+o.join("|")+" in stateview: '"+t+"@"+r.name+"'");e.resolveAs=e.resolveAs||"$resolve",e.$type="ng1",e.$context=r,e.$name=t;var n=hn.normalizeUIViewTarget(e.$context,e.$name);e.$uiViewName=n.uiViewName,e.$uiViewContextAnchor=n.uiViewContextAnchor,a[t]=e}),a}var Sr=0,kr=function(){function e(e,t,n){var r=this;this.path=e,this.viewDecl=t,this.factory=n,this.$id=Sr++,this.loaded=!1,this.getTemplate=function(e,t){return r.component?r.factory.makeComponentTemplate(e,t,r.component,r.viewDecl.bindings):r.template}}return e.prototype.load=function(){var t=this,e=V.$q,n=new Et(this.path),r=this.path.reduce(function(e,t){return N(e,t.paramValues)},{}),i={template:e.when(this.factory.fromConfig(this.viewDecl,r,n)),controller:e.when(this.getController(n))};return e.all(i).then(function(e){return ut.traceViewServiceEvent("Loaded",t),t.controller=e.controller,N(t,e.template),t})},e.prototype.getController=function(e){var t=this.viewDecl.controllerProvider;if(!M(t))return this.viewDecl.controller;var n=V.$injector.annotate(t),r=E(t)?De(t):t;return new kt("",r,n).get(e)},e}(),Dr=function(){function e(){var r=this;this._useHttp=C.version.minor<3,this.$get=["$http","$templateCache","$injector",function(e,t,n){return r.$templateRequest=n.has&&n.has("$templateRequest")&&n.get("$templateRequest"),r.$http=e,r.$templateCache=t,r}]}return e.prototype.useHttpService=function(e){this._useHttp=e},e.prototype.fromConfig=function(e,t,n){var r=function(e){return V.$q.when(e).then(function(e){return{template:e}})},i=function(e){return V.$q.when(e).then(function(e){return{component:e}})};return k(e.template)?r(this.fromString(e.template,t)):k(e.templateUrl)?r(this.fromUrl(e.templateUrl,t)):k(e.templateProvider)?r(this.fromProvider(e.templateProvider,t,n)):k(e.component)?i(e.component):k(e.componentProvider)?i(this.fromComponentProvider(e.componentProvider,t,n)):r("")},e.prototype.fromString=function(e,t){return D(e)?e(t):e},e.prototype.fromUrl=function(e,t){return D(e)&&(e=e(t)),null==e?null:this._useHttp?this.$http.get(e,{cache:this.$templateCache,headers:{Accept:"text/html"}}).then(function(e){return e.data}):this.$templateRequest(e)},e.prototype.fromProvider=function(e,t,n){var r=V.$injector.annotate(e),i=E(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.fromComponentProvider=function(e,t,n){var r=V.$injector.annotate(e),i=E(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.makeComponentTemplate=function(l,u,e,c){c=c||{};var d=3<=C.version.minor?"::":"",p=function(e){var t=je(e);return/^(x|data)-/.exec(t)?"x-"+t:t},t=function(e){var t=V.$injector.get(e+"Directive");if(!t||!t.length)throw new Error("Unable to find component named '"+e+"'");return t.map(xr).reduce(fe,[])}(e).map(function(e){var t=e.name,n=e.type,r=p(t);if(l.attr(r)&&!c[t])return r+"='"+l.attr(r)+"'";var i=c[t]||t;if("@"===n)return r+"='{{"+d+"$resolve."+i+"}}'";if("&"!==n)return r+"='"+d+"$resolve."+i+"'";var o=u.getResolvable(i),a=o&&o.data,s=a&&V.$injector.annotate(a)||[];return r+"='$resolve."+i+(E(a)?"["+(a.length-1)+"]":"")+"("+s.join(",")+")'"}).join(" "),n=p(e);return"<"+n+" "+t+">"},e}();var xr=function(e){return T(e.bindToController)?Or(e.bindToController):Or(e.scope)},Or=function(t){return Object.keys(t||{}).map(function(e){return[e,/^([=<@&])[?]?(.*)/.exec(t[e])]}).filter(function(e){return k(e)&&E(e[1])}).map(function(e){return{name:e[1][2]||e[0],type:e[1][1]}})},Tr=function(){function n(e,t){this.stateRegistry=e,this.stateService=t,B(f(n.prototype),this,f(this))}return n.prototype.decorator=function(e,t){return this.stateRegistry.decorator(e,t)||this},n.prototype.state=function(e,t){return T(e)?t=e:t.name=e,this.stateRegistry.register(t),this},n.prototype.onInvalid=function(e){return this.stateService.onInvalid(e)},n}(),Er=function(n){return function(e,t){var i=e[n],o="onExit"===n?"from":"to";return i?function(e,t){var n=new Et(e.treeChanges(o)).subContext(t.$$state()),r=N(Wr(n),{$state$:t,$transition$:e});return V.$injector.invoke(i,this,r)}:void 0}},Ar=function(){function e(e){this._urlListeners=[],this.$locationProvider=e;var t=f(e);B(t,this,t,["hashPrefix"])}return e.monkeyPatchPathParameterType=function(e){var t=e.urlMatcherFactory.type("path");t.encode=function(e){return null!=e?e.toString().replace(/(~|\/)/g,function(e){return{"~":"~~","/":"~2F"}[e]}):e},t.decode=function(e){return null!=e?e.toString().replace(/(~~|~2F)/g,function(e){return{"~~":"~","~2F":"/"}[e]}):e}},e.prototype.dispose=function(){},e.prototype.onChange=function(e){var t=this;return this._urlListeners.push(e),function(){return Q(t._urlListeners)(e)}},e.prototype.html5Mode=function(){var e=this.$locationProvider.html5Mode();return(e=T(e)?e.enabled:e)&&this.$sniffer.history},e.prototype.baseHref=function(){return this._baseHref||(this._baseHref=this.$browser.baseHref()||this.$window.location.pathname)},e.prototype.url=function(e,t,n){return void 0===t&&(t=!1),k(e)&&this.$location.url(e),t&&this.$location.replace(),n&&this.$location.state(n),this.$location.url()},e.prototype._runtimeServices=function(e,t,n,r,i){var o=this;this.$location=t,this.$sniffer=n,this.$browser=r,this.$window=i,e.$on("$locationChangeSuccess",function(t){return o._urlListeners.forEach(function(e){return e(t)})});var a=f(t);B(a,this,a,["replace","path","search","hash"]),B(a,this,a,["port","protocol","host"])},e}(),Pr=function(){function n(e){this._router=e,this._urlRouter=e.urlRouter}return n.injectableHandler=function(t,n){return function(e){return V.$injector.invoke(n,null,{$match:e,$stateParams:t.globals.params})}},n.prototype.$get=function(){var e=this._urlRouter;return e.update(!0),e.interceptDeferred||e.listen(),e},n.prototype.rule=function(e){var t=this;if(!D(e))throw new Error("'rule' must be a function");var n=new un(function(){return e(V.$injector,t._router.locationService)},U);return this._urlRouter.rule(n),this},n.prototype.otherwise=function(e){var t=this,n=this._urlRouter;if(O(e))n.otherwise(e);else{if(!D(e))throw new Error("'rule' must be a string or function");n.otherwise(function(){return e(V.$injector,t._router.locationService)})}return this},n.prototype.when=function(e,t){return(E(t)||D(t))&&(t=n.injectableHandler(this._router,t)),this._urlRouter.when(e,t),this},n.prototype.deferIntercept=function(e){this._urlRouter.deferIntercept(e)},n}();C.module("ui.router.angular1",[]);var Mr=C.module("ui.router.init",["ng"]),Rr=C.module("ui.router.util",["ui.router.init"]),Ir=C.module("ui.router.router",["ui.router.util"]),Vr=C.module("ui.router.state",["ui.router.router","ui.router.util","ui.router.angular1"]),Fr=C.module("ui.router",["ui.router.init","ui.router.state","ui.router.angular1"]),Lr=(C.module("ui.router.compat",["ui.router"]),null);function jr(e){(Lr=this.router=new Cn).stateProvider=new Tr(Lr.stateRegistry,Lr.stateService),Lr.stateRegistry.decorator("views",Cr),Lr.stateRegistry.decorator("onExit",Er("onExit")),Lr.stateRegistry.decorator("onRetain",Er("onRetain")),Lr.stateRegistry.decorator("onEnter",Er("onEnter")),Lr.viewService._pluginapi._viewConfigFactory("ng1",$r());var s=Lr.locationService=Lr.locationConfig=new Ar(e);function t(e,t,n,r,i,o,a){return s._runtimeServices(i,e,r,t,n),delete Lr.router,delete Lr.$get,Lr}return Ar.monkeyPatchPathParameterType(Lr),((Lr.router=Lr).$get=t).$inject=["$location","$browser","$window","$sniffer","$rootScope","$http","$templateCache"],Lr}jr.$inject=["$locationProvider"];var Hr=function(n){return["$uiRouterProvider",function(e){var t=e.router[n];return t.$get=function(){return t},t}]};function Yr(t,e,n){if(V.$injector=t,V.$q=e,!t.hasOwnProperty("strictDi"))try{t.invoke(function(e){})}catch(e){t.strictDi=!!/strict mode/.exec(e&&e.toString())}n.stateRegistry.get().map(function(e){return e.$$state().resolvables}).reduce(fe,[]).filter(function(e){return"deferred"===e.deps}).forEach(function(e){return e.deps=t.annotate(e.resolveFn,t.strictDi)})}Yr.$inject=["$injector","$q","$uiRouter"];function Nr(e){e.$watch(function(){ut.approximateDigests++})}Nr.$inject=["$rootScope"],Mr.provider("$uiRouter",jr),Ir.provider("$urlRouter",["$uiRouterProvider",function(e){return e.urlRouterProvider=new Pr(e)}]),Rr.provider("$urlService",Hr("urlService")),Rr.provider("$urlMatcherFactory",["$uiRouterProvider",function(){return Lr.urlMatcherFactory}]),Rr.provider("$templateFactory",function(){return new Dr}),Vr.provider("$stateRegistry",Hr("stateRegistry")),Vr.provider("$uiRouterGlobals",Hr("globals")),Vr.provider("$transitions",Hr("transitionService")),Vr.provider("$state",["$uiRouterProvider",function(){return N(Lr.stateProvider,{$get:function(){return Lr.stateService}})}]),Vr.factory("$stateParams",["$uiRouter",function(e){return e.globals.params}]),Fr.factory("$view",function(){return Lr.viewService}),Fr.service("$trace",function(){return ut}),Fr.run(Nr),Rr.run(["$urlMatcherFactory",function(e){}]),Vr.run(["$state",function(e){}]),Ir.run(["$urlRouter",function(e){}]),Mr.run(Yr);var qr,Ur,zr,Br,Wr=function(n){return n.getTokens().filter(O).map(function(e){var t=n.getResolvable(e);return[e,"NOWAIT"===n.getPolicy(t).async?t.promise:t.data]}).reduce(ke,{})};function Gr(e){var t,n=e.match(/^\s*({[^}]*})\s*$/);if(n&&(e="("+n[1]+")"),!(t=e.replace(/\n/g," ").match(/^\s*([^(]*?)\s*(\((.*)\))?\s*$/))||4!==t.length)throw new Error("Invalid state ref '"+e+"'");return{state:t[1]||null,paramExpr:t[3]||null}}function Kr(e){var t=e.parent().inheritedData("$uiView"),n=S("$cfg.path")(t);return n?De(n).state.name:void 0}function Qr(e,t,n){var r,i=n.uiState||e.current.name,o=N((r=e,{relative:Kr(t)||r.$current,inherit:!0,source:"sref"}),n.uiStateOpts||{}),a=e.href(i,n.uiStateParams,o);return{uiState:i,uiStateParams:n.uiStateParams,uiStateOpts:o,href:a}}function Zr(e){var t="[object SVGAnimatedString]"===Object.prototype.toString.call(e.prop("href")),n="FORM"===e[0].nodeName;return{attr:n?"action":t?"xlink:href":"href",isAnchor:"A"===e.prop("tagName").toUpperCase(),clickable:!n}}function Xr(o,a,s,l,u){return function(e){var t=e.which||e.button,n=u();if(!(1>>0;if(0===i)return-1;var o=+t||0;if(Math.abs(o)===1/0&&(o=0),i<=o)return-1;for(n=Math.max(0<=o?o:i-Math.abs(o),0);n
    ',this.loadingBarTemplate='
    ',this.$get=["$injector","$document","$timeout","$rootScope",function(i,o,a,s){function l(e){if(m){var t=100*e+"%";f.css("width",t),v=e,y&&(a.cancel(c),c=a(function(){n()},250))}}function n(){if(!(1<=r())){var e,t=r();e=0<=t&&t<.25?(3*Math.random()+3)/100:.25<=t&&t<.65?3*Math.random()/100:.65<=t&&t<.9?2*Math.random()/100:.9<=t&&t<.99?.005:0,l(r()+e)}}function r(){return v}function t(){v=0,m=!1}var u,c,d,p=this.parentSelector,h=angular.element(this.loadingBarTemplate),f=h.find("div").eq(0),g=angular.element(this.spinnerTemplate),m=!1,v=0,y=this.autoIncrement,w=this.includeSpinner,b=this.includeBar,$=this.startSize;return{start:function(){if(u||(u=i.get("$animate")),a.cancel(d),!m){var e=o[0],t=e.querySelector?e.querySelector(p):o.find(p)[0];t||(t=e.getElementsByTagName("body")[0]);var n=angular.element(t),r=t.lastChild&&angular.element(t.lastChild);s.$broadcast("cfpLoadingBar:started"),m=!0,b&&u.enter(h,n,r),w&&u.enter(g,n,h),l($)}},set:l,status:r,inc:n,complete:function(){u||(u=i.get("$animate")),s.$broadcast("cfpLoadingBar:completed"),l(1),a.cancel(d),d=a(function(){var e=u.leave(h,t);e&&e.then&&e.then(t),u.leave(g)},500)},autoIncrement:this.autoIncrement,includeSpinner:this.includeSpinner,latencyThreshold:this.latencyThreshold,parentSelector:this.parentSelector,startSize:this.startSize}}]})}(),angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,s,l){function e(e){for(var t in e)if(void 0!==n.style[t])return e[t]}var u=function(e,t,n){n=n||{};var r=a.defer(),i=u[n.animation?"animationEndEventName":"transitionEndEventName"],o=function(){l.$apply(function(){e.unbind(i,o),r.resolve(e)})};return i&&e.bind(i,o),s(function(){angular.isString(t)?e.addClass(t):angular.isFunction(t)?t(e):angular.isObject(t)&&e.css(t),i||r.resolve(e)}),r.promise.cancel=function(){i&&e.unbind(i,o),r.reject("Transition cancelled")},r.promise},n=document.createElement("trans");return u.transitionEndEventName=e({WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"}),u.animationEndEventName=e({WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"}),u}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(l){return{link:function(e,r,t){function n(e){function t(){a===n&&(a=void 0)}var n=l(r,e);return a&&a.cancel(),(a=n).then(t,t),n}function i(){r.removeClass("collapsing"),r.addClass("collapse in"),r.css({height:"auto"})}function o(){r.removeClass("collapsing"),r.addClass("collapse")}var a,s=!0;e.$watch(t.collapse,function(e){e?s?(s=!1,o(),r.css({height:0})):(r.css({height:r[0].scrollHeight+"px"}),r[0].offsetWidth,r.removeClass("collapse in").addClass("collapsing"),n({height:0}).then(o)):s?(s=!1,i()):(r.removeClass("collapse").addClass("collapsing"),n({height:r[0].scrollHeight+"px"}).then(i))})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(e,n,r){this.groups=[],this.closeOthers=function(t){(angular.isDefined(n.closeOthers)?e.$eval(n.closeOthers):r.closeOthers)&&angular.forEach(this.groups,function(e){e!==t&&(e.isOpen=!1)})},this.addGroup=function(e){var t=this;this.groups.push(e),e.$on("$destroy",function(){t.removeGroup(e)})},this.removeGroup=function(e){var t=this.groups.indexOf(e);-1!==t&&this.groups.splice(t,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(e){this.heading=e}},link:function(t,e,n,r){r.addGroup(t),t.$watch("isOpen",function(e){e&&r.closeOthers(t)}),t.toggleOpen=function(){t.isDisabled||(t.isOpen=!t.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(e,t,n,r,i){r.setHeading(i(e,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(e,t,n,r){e.$watch(function(){return r[n.accordionTransclude]},function(e){e&&(t.html(""),t.append(e))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(e,t){e.closeable="close"in t,this.close=e.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}).directive("dismissOnTimeout",["$timeout",function(i){return{require:"alert",link:function(e,t,n,r){i(function(){r.close()},parseInt(n.dismissOnTimeout,10))}}}]),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(e,t,n){t.addClass("ng-binding").data("$binding",n.bindHtmlUnsafe),e.$watch(n.bindHtmlUnsafe,function(e){t.html(e||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(e){this.activeClass=e.activeClass||"active",this.toggleEvent=e.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(t,n,r,e){var i=e[0],o=e[1];o.$render=function(){n.toggleClass(i.activeClass,angular.equals(o.$modelValue,t.$eval(r.btnRadio)))},n.bind(i.toggleEvent,function(){var e=n.hasClass(i.activeClass);(!e||angular.isDefined(r.uncheckable))&&t.$apply(function(){o.$setViewValue(e?null:t.$eval(r.btnRadio)),o.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(r,e,t,n){function i(){return o(t.btnCheckboxTrue,!0)}function o(e,t){var n=r.$eval(e);return angular.isDefined(n)?n:t}var a=n[0],s=n[1];s.$render=function(){e.toggleClass(a.activeClass,angular.equals(s.$modelValue,i()))},e.bind(a.toggleEvent,function(){r.$apply(function(){s.$setViewValue(e.hasClass(a.activeClass)?o(t.btnCheckboxFalse,!1):i()),s.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$interval","$transition",function(a,t,n,s){function l(){r();var e=+a.interval;!isNaN(e)&&0=d.length?d[t-1]:d[t]):t
    ");e.attr({"ng-model":"date","ng-change":"dateSelection()"});var c=angular.element(e.children()[0]);i.datepickerOptions&&angular.forEach(r.$parent.$eval(i.datepickerOptions),function(e,t){c.attr(o(t),e)}),r.watchData={},angular.forEach(["minDate","maxDate","datepickerMode"],function(t){if(i[t]){var e=g(i[t]);if(r.$parent.$watch(e,function(e){r.watchData[t]=e}),c.attr(o(t),"watchData."+t),"datepickerMode"===t){var n=e.assign;r.$watch("watchData."+t,function(e,t){e!==t&&n(r.$parent,e)})}}}),i.dateDisabled&&c.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),n.$parsers.unshift(a),r.dateSelection=function(e){angular.isDefined(e)&&(r.date=e),n.$setViewValue(r.date),n.$render(),l&&(r.isOpen=!1,t[0].focus())},t.bind("input change keyup",function(){r.$apply(function(){r.date=n.$modelValue})}),n.$render=function(){var e=n.$viewValue?y(n.$viewValue,s):"";t.val(e),r.date=a(n.$modelValue)};var d=function(e){r.isOpen&&e.target!==t[0]&&r.$apply(function(){r.isOpen=!1})},p=function(e){r.keydown(e)};t.bind("keydown",p),r.keydown=function(e){27===e.which?(e.preventDefault(),e.stopPropagation(),r.close()):40!==e.which||r.isOpen||(r.isOpen=!0)},r.$watch("isOpen",function(e){e?(r.$broadcast("datepicker.focus"),r.position=u?v.offset(t):v.position(t),r.position.top=r.position.top+t.prop("offsetHeight"),m.bind("click",d)):m.unbind("click",d)}),r.select=function(e){if("today"===e){var t=new Date;angular.isDate(n.$modelValue)?(e=new Date(n.$modelValue)).setFullYear(t.getFullYear(),t.getMonth(),t.getDate()):e=new Date(t.setHours(0,0,0,0))}r.dateSelection(e)},r.close=function(){r.isOpen=!1,t[0].focus()};var h=f(e)(r);e.remove(),u?m.find("body").append(h):t.after(h),r.$on("$destroy",function(){h.remove(),t.unbind("keydown",p),m.unbind("click",d)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(e,t){t.bind("click",function(e){e.preventDefault(),e.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(t){var n=null;this.open=function(e){n||(t.bind("click",r),t.bind("keydown",i)),n&&n!==e&&(n.isOpen=!1),n=e},this.close=function(e){n===e&&(n=null,t.unbind("click",r),t.unbind("keydown",i))};var r=function(e){if(n){var t=n.getToggleElement();e&&t&&t[0].contains(e.target)||n.$apply(function(){n.isOpen=!1})}},i=function(e){27===e.which&&(n.focusToggleElement(),r())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(n,t,r,e,i,o){var a,s=this,l=n.$new(),u=e.openClass,c=angular.noop,d=t.onToggle?r(t.onToggle):angular.noop;this.init=function(e){s.$element=e,t.isOpen&&(a=r(t.isOpen),c=a.assign,n.$watch(a,function(e){l.isOpen=!!e}))},this.toggle=function(e){return l.isOpen=arguments.length?!!e:!l.isOpen},this.isOpen=function(){return l.isOpen},l.getToggleElement=function(){return s.toggleElement},l.focusToggleElement=function(){s.toggleElement&&s.toggleElement[0].focus()},l.$watch("isOpen",function(e,t){o[e?"addClass":"removeClass"](s.$element,u),e?(l.focusToggleElement(),i.open(l)):i.close(l),c(n,e),angular.isDefined(e)&&e!==t&&d(n,{open:!!e})}),n.$on("$locationChangeSuccess",function(){l.isOpen=!1}),n.$on("$destroy",function(){l.$destroy()})}]).directive("dropdown",function(){return{controller:"DropdownController",link:function(e,t,n,r){r.init(t)}}}).directive("dropdownToggle",function(){return{require:"?^dropdown",link:function(t,n,r,i){if(i){i.toggleElement=n;var e=function(e){e.preventDefault(),n.hasClass("disabled")||r.disabled||t.$apply(function(){i.toggle()})};n.bind("click",e),n.attr({"aria-haspopup":!0,"aria-expanded":!1}),t.$watch(i.isOpen,function(e){n.attr("aria-expanded",!!e)}),t.$on("$destroy",function(){n.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var r=[];return{add:function(e,t){r.push({key:e,value:t})},get:function(e){for(var t=0;t");i.attr("backdrop-class",t.backdropClass),h=c(i)(f),n.append(h)}var o=angular.element("
    ");o.attr({"template-url":t.windowTemplateUrl,"window-class":t.windowClass,size:t.size,index:m.length()-1,animate:"animate"}).html(t.content);var a=c(o)(t.scope);m.top().value.modalDomEl=a,n.append(a),n.addClass(g)},n.close=function(e,t){var n=m.get(e);n&&(n.value.deferred.resolve(t),r(e))},n.dismiss=function(e,t){var n=m.get(e);n&&(n.value.deferred.reject(t),r(e))},n.dismissAll=function(e){for(var t=this.getTop();t;)this.dismiss(t.key,e),t=this.getTop()},n.getTop=function(){return m.top()},n}]).provider("$modal",function(){var g={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(l,u,c,d,p,h,f){var e={};return e.open=function(o){var a=c.defer(),e=c.defer(),s={result:a.promise,opened:e.promise,close:function(e){f.close(s,e)},dismiss:function(e){f.dismiss(s,e)}};if((o=angular.extend({},g.options,o)).resolve=o.resolve||{},!o.template&&!o.templateUrl)throw new Error("One of template or templateUrl options is required.");var t,n,r,i=c.all([(r=o,r.template?c.when(r.template):d.get(angular.isFunction(r.templateUrl)?r.templateUrl():r.templateUrl,{cache:p}).then(function(e){return e.data}))].concat((t=o.resolve,n=[],angular.forEach(t,function(e){(angular.isFunction(e)||angular.isArray(e))&&n.push(c.when(l.invoke(e)))}),n)));return i.then(function(n){var e=(o.scope||u).$new();e.$close=s.close,e.$dismiss=s.dismiss;var t,r={},i=1;o.controller&&(r.$scope=e,r.$modalInstance=s,angular.forEach(o.resolve,function(e,t){r[t]=n[i++]}),t=h(o.controller,r),o.controllerAs&&(e[o.controllerAs]=t)),f.open(s,{scope:e,deferred:a,content:n[0],backdrop:o.backdrop,keyboard:o.keyboard,backdropClass:o.backdropClass,windowClass:o.windowClass,windowTemplateUrl:o.windowTemplateUrl,size:o.size})},function(e){a.reject(e)}),i.then(function(){e.resolve(!0)},function(){e.reject(!1)}),s},e}]};return g}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(n,r,i){var o=this,a={$setViewValue:angular.noop},t=r.numPages?i(r.numPages).assign:angular.noop;this.init=function(e,t){a=e,this.config=t,a.$render=function(){o.render()},r.itemsPerPage?n.$parent.$watch(i(r.itemsPerPage),function(e){o.itemsPerPage=parseInt(e,10),n.totalPages=o.calculateTotalPages()}):this.itemsPerPage=t.itemsPerPage},this.calculateTotalPages=function(){var e=this.itemsPerPage<1?1:Math.ceil(n.totalItems/this.itemsPerPage);return Math.max(e||0,1)},this.render=function(){n.page=parseInt(a.$viewValue,10)||1},n.selectPage=function(e){n.page!==e&&0e?n.selectPage(e):a.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(s,l){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(e,t,n,r){function c(e,t,n){return{number:e,text:t,active:n}}var i=r[0],o=r[1];if(o){var d=angular.isDefined(n.maxSize)?e.$parent.$eval(n.maxSize):l.maxSize,p=angular.isDefined(n.rotate)?e.$parent.$eval(n.rotate):l.rotate;e.boundaryLinks=angular.isDefined(n.boundaryLinks)?e.$parent.$eval(n.boundaryLinks):l.boundaryLinks,e.directionLinks=angular.isDefined(n.directionLinks)?e.$parent.$eval(n.directionLinks):l.directionLinks,i.init(o,l),n.maxSize&&e.$parent.$watch(s(n.maxSize),function(e){d=parseInt(e,10),i.render()});var a=i.render;i.render=function(){a(),0';return{restrict:"EA",compile:function(){var _=o(i);return function(r,t,i){function e(){m.isOpen?o():n()}function n(){var e,t,n;(!g||r.$eval(i[S+"Enable"]))&&(n=i[S+"Placement"],m.placement=angular.isDefined(n)?n:D.placement,e=i[S+"PopupDelay"],t=parseInt(e,10),m.popupDelay=isNaN(t)?D.popupDelay:t,m.popupDelay?p||(p=x(a,m.popupDelay,!1)).then(function(e){e()}):a()())}function o(){r.$apply(function(){s()})}function a(){return p=null,d&&(x.cancel(d),d=null),m.content?(u&&l(),c=m.$new(),(u=_(c,function(e){h?O.find("body").append(e):t.after(e)})).css({top:0,left:0,display:"block"}),m.$digest(),v(),m.isOpen=!0,m.$digest(),v):angular.noop}function s(){m.isOpen=!1,x.cancel(p),p=null,m.animation?d||(d=x(l,500)):l()}function l(){d=null,u&&(u.remove(),u=null),c&&(c.$destroy(),c=null)}var u,c,d,p,h=!!angular.isDefined(D.appendToBody)&&D.appendToBody,f=k(void 0),g=angular.isDefined(i[S+"Enable"]),m=r.$new(!0),v=function(){var e=T.positionElements(t,u,m.placement,h);e.top+="px",e.left+="px",u.css(e)};m.isOpen=!1,i.$observe(C,function(e){!(m.content=e)&&m.isOpen&&s()}),i.$observe(S+"Title",function(e){m.title=e});var y,w=function(){t.unbind(f.show,n),t.unbind(f.hide,o)};y=i[S+"Trigger"],w(),(f=k(y)).show===f.hide?t.bind(f.show,e):(t.bind(f.show,n),t.bind(f.hide,o));var b=r.$eval(i[S+"Animation"]);m.animation=angular.isDefined(b)?!!b:D.animation;var $=r.$eval(i[S+"AppendToBody"]);(h=angular.isDefined($)?$:h)&&r.$on("$locationChangeSuccess",function(){m.isOpen&&s()}),r.$on("$destroy",function(){x.cancel(d),x.cancel(p),w(),l(),m=null})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(e){return e("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(e){return e("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(e){return e("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(n,e,t){var r=this,i=angular.isDefined(e.animate)?n.$parent.$eval(e.animate):t.animate;this.bars=[],n.max=angular.isDefined(e.max)?n.$parent.$eval(e.max):t.max,this.addBar=function(t,e){i||e.css({transition:"none"}),this.bars.push(t),t.$watch("value",function(e){t.percent=+(100*e/n.max).toFixed(2)}),t.$on("$destroy",function(){e=null,r.removeBar(t)})},this.removeBar=function(e){this.bars.splice(this.bars.indexOf(e),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(e,t,n,r){r.addBar(e,t)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(e,t,n,r){r.addBar(e,angular.element(t.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(n,r,i){var o={$setViewValue:angular.noop};this.init=function(e){(o=e).$render=this.render,this.stateOn=angular.isDefined(r.stateOn)?n.$parent.$eval(r.stateOn):i.stateOn,this.stateOff=angular.isDefined(r.stateOff)?n.$parent.$eval(r.stateOff):i.stateOff;var t=angular.isDefined(r.ratingStates)?n.$parent.$eval(r.ratingStates):new Array(angular.isDefined(r.max)?n.$parent.$eval(r.max):i.max);n.range=this.buildTemplateObjects(t)},this.buildTemplateObjects=function(e){for(var t=0,n=e.length;t");v.attr({id:t,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(e.typeaheadTemplateUrl)&&v.attr("template-url",e.typeaheadTemplateUrl);var y=function(){m.matches=[],m.activeIdx=-1,a.attr("aria-expanded",!1)},w=function(e){return t+"-option-"+e};m.$watch("activeIdx",function(e){e<0?a.removeAttr("aria-activedescendant"):a.attr("aria-activedescendant",w(e))});var b=function(r){var i={$viewValue:r};u(o,!0),x.when(g.source(o,i)).then(function(e){var t=r===s.$viewValue;if(t&&l)if(0=n?0$&"):e}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion-group.html",'
    \n
    \n

    \n {{heading}}\n

    \n
    \n
    \n\t
    \n
    \n
    \n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion.html",'
    ')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(e){e.put("template/alert/alert.html",'\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(e){e.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(e){e.put("template/carousel/slide.html","
    \n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/datepicker.html",'
    \n \n \n \n
    ')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{label.abbr}}
    {{ weekNumbers[$index] }}\n \n
    \n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/popup.html",'\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(e){e.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(e){e.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-html-unsafe-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(e){e.put("template/popover/popover.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/bar.html",'
    ')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progress.html",'
    ')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progressbar.html",'
    \n
    \n
    ')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(e){e.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tabset.html",'
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(e){e.put("template/timepicker/timepicker.html",'\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
     
    \n\t\t\t\t\n\t\t\t:\n\t\t\t\t\n\t\t\t
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-popup.html",'\n')}]),function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function p(){return e.apply(null,arguments)}function s(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function l(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function u(e){return void 0===e}function c(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function h(e,t){var n,r=[];for(n=0;n>>0,r=0;rCe(e)?(o=e+1,s-Ce(e)):(o=e,s),{year:o,dayOfYear:a}}function Ne(e,t,n){var r,i,o=He(e.year(),t,n),a=Math.floor((e.dayOfYear()-o-1)/7)+1;return a<1?r=a+qe(i=e.year()-1,t,n):a>qe(e.year(),t,n)?(r=a-qe(e.year(),t,n),i=e.year()+1):(i=e.year(),r=a),{week:r,year:i}}function qe(e,t,n){var r=He(e,t,n),i=He(e+1,t,n);return(Ce(e)-r+i)/7}N("w",["ww",2],"wo","week"),N("W",["WW",2],"Wo","isoWeek"),P("week","w"),P("isoWeek","W"),V("week",5),V("isoWeek",5),le("w",Q),le("ww",Q,B),le("W",Q),le("WW",Q,B),he(["w","ww","W","WW"],function(e,t,n,r){t[r.substr(0,1)]=S(e)});N("d",0,"do","day"),N("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),N("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),N("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),N("e",0,0,"weekday"),N("E",0,0,"isoWeekday"),P("day","d"),P("weekday","e"),P("isoWeekday","E"),V("day",11),V("weekday",11),V("isoWeekday",11),le("d",Q),le("e",Q),le("E",Q),le("dd",function(e,t){return t.weekdaysMinRegex(e)}),le("ddd",function(e,t){return t.weekdaysShortRegex(e)}),le("dddd",function(e,t){return t.weekdaysRegex(e)}),he(["dd","ddd","dddd"],function(e,t,n,r){var i=n._locale.weekdaysParse(e,r,n._strict);null!=i?t.d=i:v(n).invalidWeekday=e}),he(["d","e","E"],function(e,t,n,r){t[r]=S(e)});var Ue="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var Be="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var We=ae;var Ge=ae;var Ke=ae;function Qe(){function e(e,t){return t.length-e.length}var t,n,r,i,o,a=[],s=[],l=[],u=[];for(t=0;t<7;t++)n=m([2e3,1]).day(t),r=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),o=this.weekdays(n,""),a.push(r),s.push(i),l.push(o),u.push(r),u.push(i),u.push(o);for(a.sort(e),s.sort(e),l.sort(e),u.sort(e),t=0;t<7;t++)s[t]=ce(s[t]),l[t]=ce(l[t]),u[t]=ce(u[t]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Ze(){return this.hours()%12||12}function Xe(e,t){N(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function Je(e,t){return t._meridiemParse}N("H",["HH",2],0,"hour"),N("h",["hh",2],0,Ze),N("k",["kk",2],0,function(){return this.hours()||24}),N("hmm",0,0,function(){return""+Ze.apply(this)+F(this.minutes(),2)}),N("hmmss",0,0,function(){return""+Ze.apply(this)+F(this.minutes(),2)+F(this.seconds(),2)}),N("Hmm",0,0,function(){return""+this.hours()+F(this.minutes(),2)}),N("Hmmss",0,0,function(){return""+this.hours()+F(this.minutes(),2)+F(this.seconds(),2)}),Xe("a",!0),Xe("A",!1),P("hour","h"),V("hour",13),le("a",Je),le("A",Je),le("H",Q),le("h",Q),le("k",Q),le("HH",Q,B),le("hh",Q,B),le("kk",Q,B),le("hmm",Z),le("hmmss",X),le("Hmm",Z),le("Hmmss",X),pe(["H","HH"],ve),pe(["k","kk"],function(e,t,n){var r=S(e);t[ve]=24===r?0:r}),pe(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),pe(["h","hh"],function(e,t,n){t[ve]=S(e),v(n).bigHour=!0}),pe("hmm",function(e,t,n){var r=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r)),v(n).bigHour=!0}),pe("hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r,2)),t[we]=S(e.substr(i)),v(n).bigHour=!0}),pe("Hmm",function(e,t,n){var r=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r))}),pe("Hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r,2)),t[we]=S(e.substr(i))});var et,tt=xe("Hours",!0),nt={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Pe,monthsShort:Me,week:{dow:0,doy:6},weekdays:Ue,weekdaysMin:Be,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},rt={},it={};function ot(e){return e?e.toLowerCase().replace("_","-"):e}function at(e){var t=null;if(!rt[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=et._abbr,require("./locale/"+e),st(t)}catch(e){}return rt[e]}function st(e,t){var n;return e&&((n=u(t)?ut(e):lt(e,t))?et=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),et._abbr}function lt(e,t){if(null===t)return delete rt[e],null;var n,r=nt;if(t.abbr=e,null!=rt[e])x("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),r=rt[e]._config;else if(null!=t.parentLocale)if(null!=rt[t.parentLocale])r=rt[t.parentLocale]._config;else{if(null==(n=at(t.parentLocale)))return it[t.parentLocale]||(it[t.parentLocale]=[]),it[t.parentLocale].push({name:e,config:t}),null;r=n._config}return rt[e]=new E(T(r,t)),it[e]&&it[e].forEach(function(e){lt(e.name,e.config)}),st(e),rt[e]}function ut(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return et;if(!s(e)){if(t=at(e))return t;e=[e]}return function(e){for(var t,n,r,i,o=0;o=t&&a(i,n,!0)>=t-1)break;t--}o++}return et}(e)}function ct(e){var t,n=e._a;return n&&-2===v(e).overflow&&(t=n[ge]<0||11Ee(n[fe],n[ge])?me:n[ve]<0||24qe(n,o,a)?v(e)._overflowWeeks=!0:null!=l?v(e)._overflowWeekday=!0:(s=Ye(n,r,i,o,a),e._a[fe]=s.year,e._dayOfYear=s.dayOfYear)}(e),null!=e._dayOfYear&&(o=dt(e._a[fe],r[fe]),(e._dayOfYear>Ce(o)||0===e._dayOfYear)&&(v(e)._overflowDayOfYear=!0),n=je(o,0,e._dayOfYear),e._a[ge]=n.getUTCMonth(),e._a[me]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=r[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ve]&&0===e._a[ye]&&0===e._a[we]&&0===e._a[be]&&(e._nextDay=!0,e._a[ve]=0),e._d=(e._useUTC?je:function(e,t,n,r,i,o,a){var s=new Date(e,t,n,r,i,o,a);return e<100&&0<=e&&isFinite(s.getFullYear())&&s.setFullYear(e),s}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ve]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(v(e).weekdayMismatch=!0)}}var ht=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ft=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,gt=/Z|[+-]\d\d(?::?\d\d)?/,mt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],vt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],yt=/^\/?Date\((\-?\d+)/i;function wt(e){var t,n,r,i,o,a,s=e._i,l=ht.exec(s)||ft.exec(s);if(l){for(v(e).iso=!0,t=0,n=mt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},un.isLocal=function(){return!!this.isValid()&&!this._isUTC},un.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},un.isUtc=Ht,un.isUTC=Ht,un.zoneAbbr=function(){return this._isUTC?"UTC":""},un.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},un.dates=n("dates accessor is deprecated. Use date instead.",nn),un.months=n("months accessor is deprecated. Use month instead",Ie),un.years=n("years accessor is deprecated. Use year instead",De),un.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),un.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!u(this._isDSTShifted))return this._isDSTShifted;var e={};if(b(e,this),(e=kt(e))._a){var t=e._isUTC?m(e._a):xt(e._a);this._isDSTShifted=this.isValid()&&0-1/0&&t.selectable&&c[e]){var r=c[e](t.utcDateValue),i=[];if(r.weeks)for(var o=0;on+9,past:u.year()r.indexOf(e.startView))throw new Error("startView must be greater than minView");if(!a.isNumber(e.minuteStep))throw new Error("minuteStep must be numeric");if(e.minuteStep<=0||60<=e.minuteStep)throw new Error("minuteStep must be greater than zero and less than 60");if(null!==e.configureOn&&!a.isString(e.configureOn))throw new Error("configureOn must be a string");if(null!==e.configureOn&&e.configureOn.length<1)throw new Error("configureOn must not be an empty string");if(null!==e.renderOn&&!a.isString(e.renderOn))throw new Error("renderOn must be a string");if(null!==e.renderOn&&e.renderOn.length<1)throw new Error("renderOn must not be an empty string");if(null!==e.modelType&&!a.isString(e.modelType))throw new Error("modelType must be a string");if(null!==e.modelType&&e.modelType.length<1)throw new Error("modelType must not be an empty string");"Date"!==e.modelType&&"moment"!==e.modelType&&"milliseconds"!==e.modelType&&(e.parseFormat=e.modelType);if(null!==e.dropdownSelector&&!a.isString(e.dropdownSelector))throw new Error("dropdownSelector must be a string");null===e.dropdownSelector||"undefined"!=typeof jQuery&&"function"==typeof jQuery().dropdown||(i.error("Please DO NOT specify the dropdownSelector option unless you are using jQuery AND Bootstrap.js. Please include jQuery AND Bootstrap.js, or write code to close the dropdown in the on-set-time callback. \n\nThe dropdownSelector configuration option is being removed because it will not function properly."),delete e.dropdownSelector)}}}a.module("ui.bootstrap.datetimepicker",[]).service("dateTimePickerConfig",function(){var e={bg:{previous:"предишна",next:"следваща"},ca:{previous:"anterior",next:"següent"},da:{previous:"forrige",next:"næste"},de:{previous:"vorige",next:"weiter"},"en-au":{previous:"previous",next:"next"},"en-gb":{previous:"previous",next:"next"},en:{previous:"previous",next:"next"},"es-us":{previous:"atrás",next:"siguiente"},es:{previous:"atrás",next:"siguiente"},fi:{previous:"edellinen",next:"seuraava"},fr:{previous:"précédent",next:"suivant"},hu:{previous:"előző",next:"következő"},it:{previous:"precedente",next:"successivo"},ja:{previous:"前へ",next:"次へ"},ml:{previous:"മുൻപുള്ളത്",next:"അടുത്തത്"},nl:{previous:"vorige",next:"volgende"},pl:{previous:"poprzednia",next:"następna"},"pt-br":{previous:"anteriores",next:"próximos"},pt:{previous:"anterior",next:"próximo"},ro:{previous:"anterior",next:"următor"},ru:{previous:"предыдущая",next:"следующая"},sk:{previous:"predošlá",next:"ďalšia"},sv:{previous:"föregående",next:"nästa"},tr:{previous:"önceki",next:"sonraki"},uk:{previous:"назад",next:"далі"},"zh-cn":{previous:"上一页",next:"下一页"},"zh-tw":{previous:"上一頁",next:"下一頁"}}[b.locale().toLowerCase()];return a.extend({},{configureOn:null,dropdownSelector:null,minuteStep:5,minView:"minute",modelType:"Date",parseFormat:"YYYY-MM-DDTHH:mm:ss.SSSZZ",renderOn:null,startView:"day"},{screenReader:e})}).service("dateTimePickerValidator",t).directive("datetimepicker",e),e.$inject=["dateTimePickerConfig","dateTimePickerValidator"],t.$inject=["$log"]}),angular.module("rzTable",[]),angular.module("rzTable").directive("rzTable",["resizeStorage","$injector","$parse",function(g,i,u){function e(e){}function c(n,r,i){return function(e,t){!0!==i.busy&&void 0!==t&&t!==e&&(d(n),p(n,r,i))}}function d(e){k=!0,l.map(function(e){e.remove()}),l=[]}function p(e,t,n){if(!n.busy){b=$(e).find("th"),v=n.mode,y=!angular.isDefined(n.saveTableSizes)||n.saveTableSizes,w=n.profile;var r=function(t,e){try{var n=e.rzMode?t.mode:"BasicResizer",r=i.get(n);return r}catch(e){return console.error("The resizer "+t.mode+" was not found"),null}}(n,t);r&&(S=new r(e,b,h),y&&(D=g.loadTableSizes(e,n.mode,n.profile)),s=S.handles(b),a=S.ctrlColumns,S.setup(),(o=D)&&($(C).width("auto"),a.each(function(e,t){var n=angular.element(t).scope(),r=n.rzCol||$(t).attr("id"),i=o[r];$(t).css({width:i})}),S.onTableReady()),s.each(function(e,t){!function(e,t,n){var r=$("
    ",{class:e.options.handleClass||"rz-handle"});$(n).prepend(r),l.push(r);var i=S.handleMiddleware(r,n);p=e,h=r,f=i,$(h).mousedown(function(e){k&&(S.onFirstDrag(f,h),S.onTableReady(),k=!1),p.options.onResizeStarted&&p.options.onResizeStarted(f);var t={};S.intervene&&(((t=S.intervene.selector(f)).column=t).orgWidth=$(t).width()),e.preventDefault(),$(h).addClass(p.options.handleClassActive||"rz-handle-active");var n,r,i,o,a,s,l,u,c=e.clientX,d=$(f).width();o=p,a=f,s=c,l=d,u=t,_=function(e){var t=e.clientX,n=t-s,r=S.calculate(l,n);if(!(rt)||(this.fixedColumn.width()<=this.getMinWidth(this.fixedColumn)?(this.bound=t,$(this.fixedColumn).width(this.minWidth),!0):void 0)},e.prototype.onEndDrag=function(){this.bound=!1},e.prototype.calculate=function(e,t){return e-t},e}]),angular.module("rzTable").factory("OverflowResizer",["ResizerModel",function(r){function e(e,t,n){r.call(this,e,t,n)}return(e.prototype=Object.create(r.prototype)).setup=function(){$(this.container).css({overflow:"auto"})},e.prototype.onTableReady=function(){$(this.table).width(1)},e}]),function(e,t){"function"==typeof define&&define.amd?define(["angular"],t):"object"==typeof module&&module.exports?module.exports=t(require("angular")):e.angularClipboard=t(e.angular)}(this,function(i){return i.module("angular-clipboard",[]).factory("clipboard",["$document","$window",function(s,l){return{copyText:function(e,t){var n,r,i=l.pageXOffset||s[0].documentElement.scrollLeft,o=l.pageYOffset||s[0].documentElement.scrollTop,a=(n=e,(r=s[0].createElement("textarea")).style.position="absolute",r.style.fontSize="12pt",r.style.border="0",r.style.padding="0",r.style.margin="0",r.style.left="-10000px",r.style.top=(l.pageYOffset||s[0].documentElement.scrollTop)+"px",r.textContent=n,r);s[0].body.appendChild(a),function(e){try{s[0].body.style.webkitUserSelect="initial";var t=s[0].getSelection();t.removeAllRanges();var n=document.createRange();n.selectNodeContents(e),t.addRange(n),e.select(),e.setSelectionRange(0,999999);try{if(!s[0].execCommand("copy"))throw"failure copy"}finally{t.removeAllRanges()}}finally{s[0].body.style.webkitUserSelect=""}}(a),l.scrollTo(i,o),s[0].body.removeChild(a)},supported:"queryCommandSupported"in s[0]&&s[0].queryCommandSupported("copy")}}]).directive("clipboard",["clipboard",function(r){return{restrict:"A",scope:{onCopied:"&",onError:"&",text:"=",supported:"=?"},link:function(t,n){t.supported=r.supported,n.on("click",function(e){try{r.copyText(t.text,n[0]),i.isFunction(t.onCopied)&&t.$evalAsync(t.onCopied())}catch(e){i.isFunction(t.onError)&&t.$evalAsync(t.onError({err:e}))}})}}}])}),function(e,t){"function"==typeof define&&define.amd?define("sifter",t):"object"==typeof exports?module.exports=t():e.Sifter=t()}(this,function(){var e=function(e,t){this.items=e,this.settings=t||{diacritics:!0}};e.prototype.tokenize=function(e){if(!(e=s(String(e||"").toLowerCase()))||!e.length)return[];var t,n,r,i,o=[],a=e.split(/ +/);for(t=0,n=a.length;t/g,">").replace(/"/g,""")},t={before:function(e,t,n){var r=e[t];e[t]=function(){return n.apply(e,arguments),r.apply(e,arguments)}},after:function(t,e,n){var r=t[e];t[e]=function(){var e=r.apply(t,arguments);return n.apply(t,arguments),e}}},n=function(t,n,e){var r,i=t.trigger,o={};for(r in t.trigger=function(){var e=arguments[0];if(-1===n.indexOf(e))return i.apply(t,arguments);o[e]=arguments},e.apply(t,[]),t.trigger=i,o)o.hasOwnProperty(r)&&i.apply(t,o[r])},f=function(e){var t={};if("selectionStart"in e)t.start=e.selectionStart,t.length=e.selectionEnd-t.start;else if(document.selection){e.focus();var n=document.selection.createRange(),r=document.selection.createRange().text.length;n.moveStart("character",-e.value.length),t.start=n.text.length-r,t.length=r}return t},O=function(p){var h=null,e=function(e,t){var n,r,i,o,a,s,l,u,c,d;(t=t||{},(e=e||window.event||{}).metaKey||e.altKey)||(t.force||!1!==p.data("grow"))&&(n=p.val(),e.type&&"keydown"===e.type.toLowerCase()&&(i=48<=(r=e.keyCode)&&r<=57||65<=r&&r<=90||96<=r&&r<=111||186<=r&&r<=222||32===r,46===r||8===r?(u=f(p[0])).length?n=n.substring(0,u.start)+n.substring(u.start+u.length):8===r&&u.start?n=n.substring(0,u.start-1)+n.substring(u.start+1):46===r&&void 0!==u.start&&(n=n.substring(0,u.start)+n.substring(u.start+1)):i&&(s=e.shiftKey,l=String.fromCharCode(e.keyCode),n+=l=s?l.toUpperCase():l.toLowerCase())),o=p.attr("placeholder"),!n&&o&&(n=o),d=p,(a=((c=n)?(w.$testInput||(w.$testInput=S("").css({position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}).appendTo("body")),w.$testInput.text(c),function(e,t,n){var r,i,o={};if(n)for(r=0,i=n.length;r").addClass(g.wrapperClass).addClass(s).addClass(a),t=S("
    ").addClass(g.inputClass).addClass("items").appendTo(e),n=S('').appendTo(t).attr("tabindex",w.is(":disabled")?"-1":f.tabIndex),o=S(g.dropdownParent||e),r=S("
    ").addClass(g.dropdownClass).addClass(a).hide().appendTo(o),i=S("
    ").addClass(g.dropdownContentClass).appendTo(r),(u=w.attr("id"))&&(n.attr("id",u+"-selectized"),S("label[for='"+u+"']").attr("for",u+"-selectized")),f.settings.copyClassesToDropdown&&r.addClass(s),e.css({width:w[0].style.width}),f.plugins.names.length&&(l="plugin-"+f.plugins.names.join(" plugin-"),e.addClass(l),r.addClass(l)),(null===g.maxItems||1[data-selectable]",function(e){e.stopImmediatePropagation()}),r.on("mouseenter","[data-selectable]",function(){return f.onOptionHover.apply(f,arguments)}),r.on("mousedown click","[data-selectable]",function(){return f.onOptionSelect.apply(f,arguments)}),d="mousedown",p="*:not(input)",h=function(){return f.onItemSelect.apply(f,arguments)},(c=t).on(d,p,function(e){for(var t=e.target;t&&t.parentNode!==c[0];)t=t.parentNode;return e.currentTarget=t,h.apply(this,[e])}),O(n),t.on({mousedown:function(){return f.onMouseDown.apply(f,arguments)},click:function(){return f.onClick.apply(f,arguments)}}),n.on({mousedown:function(e){e.stopPropagation()},keydown:function(){return f.onKeyDown.apply(f,arguments)},keyup:function(){return f.onKeyUp.apply(f,arguments)},keypress:function(){return f.onKeyPress.apply(f,arguments)},resize:function(){f.positionDropdown.apply(f,[])},blur:function(){return f.onBlur.apply(f,arguments)},focus:function(){return f.ignoreBlur=!1,f.onFocus.apply(f,arguments)},paste:function(){return f.onPaste.apply(f,arguments)}}),y.on("keydown"+m,function(e){f.isCmdDown=e[$?"metaKey":"ctrlKey"],f.isCtrlDown=e[$?"altKey":"ctrlKey"],f.isShiftDown=e.shiftKey}),y.on("keyup"+m,function(e){e.keyCode===C&&(f.isCtrlDown=!1),16===e.keyCode&&(f.isShiftDown=!1),e.keyCode===_&&(f.isCmdDown=!1)}),y.on("mousedown"+m,function(e){if(f.isFocused){if(e.target===f.$dropdown[0]||e.target.parentNode===f.$dropdown[0])return!1;f.$control.has(e.target).length||e.target===f.$control[0]||f.blur(e.target)}}),v.on(["scroll"+m,"resize"+m].join(" "),function(){f.isOpen&&f.positionDropdown.apply(f,arguments)}),v.on("mousemove"+m,function(){f.ignoreHover=!1}),this.revertSettings={$children:w.children().detach(),tabindex:w.attr("tabindex")},w.attr("tabindex",-1).hide().after(f.$wrapper),S.isArray(g.items)&&(f.setValue(g.items),delete g.items),D&&w.on("invalid"+m,function(e){e.preventDefault(),f.isInvalid=!0,f.refreshState()}),f.updateOriginalInput(),f.refreshItems(),f.refreshState(),f.updatePlaceholder(),f.isSetup=!0,w.is(":disabled")&&f.disable(),f.on("change",this.onChange),w.data("selectize",f),w.addClass("selectized"),f.trigger("initialize"),!0===g.preload&&f.onSearchChange("")},setupTemplates:function(){var n=this.settings.labelField,r=this.settings.optgroupLabelField,e={optgroup:function(e){return'
    '+e.html+"
    "},optgroup_header:function(e,t){return'
    '+t(e[r])+"
    "},option:function(e,t){return'
    '+t(e[n])+"
    "},item:function(e,t){return'
    '+t(e[n])+"
    "},option_create:function(e,t){return'
    Add '+t(e.input)+"
    "}};this.settings.render=S.extend({},e,this.settings.render)},setupCallbacks:function(){var e,t,n={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(e in n)n.hasOwnProperty(e)&&(t=this.settings[n[e]])&&this.on(e,t)},onClick:function(e){this.isFocused&&this.isOpen||(this.focus(),e.preventDefault())},onMouseDown:function(e){var t=this,n=e.isDefaultPrevented();S(e.target);if(t.isFocused){if(e.target!==t.$control_input[0])return"single"===t.settings.mode?t.isOpen?t.close():t.open():n||t.setActiveItem(null),!1}else n||window.setTimeout(function(){t.focus()},0)},onChange:function(){this.$input.trigger("change")},onPaste:function(e){var i=this;i.isFull()||i.isInputHidden||i.isLocked?e.preventDefault():i.settings.splitOn&&setTimeout(function(){var e=i.$control_input.val();if(e.match(i.settings.splitOn))for(var t=S.trim(e).split(i.settings.splitOn),n=0,r=t.length;n=this.settings.maxItems},updateOriginalInput:function(e){var t,n,r,i,o=this;if(e=e||{},1===o.tagType){for(r=[],t=0,n=o.items.length;t'+s(i)+"");r.length||this.$input.attr("multiple")||r.push(''),o.$input.html(r.join(""))}else o.$input.val(o.getValue()),o.$input.attr("value",o.$input.val());o.isSetup&&(e.silent||o.trigger("change",o.$input.val()))},updatePlaceholder:function(){if(this.settings.placeholder){var e=this.$control_input;this.items.length?e.removeAttr("placeholder"):e.attr("placeholder",this.settings.placeholder),e.triggerHandler("update",{force:!0})}},open:function(){var e=this;e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.focus(),e.isOpen=!0,e.refreshState(),e.$dropdown.css({visibility:"hidden",display:"block"}),e.positionDropdown(),e.$dropdown.css({visibility:"visible"}),e.trigger("dropdown_open",e.$dropdown))},close:function(){var e=this,t=e.isOpen;"single"===e.settings.mode&&e.items.length&&(e.hideInput(),e.isBlurring||e.$control_input.blur()),e.isOpen=!1,e.$dropdown.hide(),e.setActiveOption(null),e.refreshState(),t&&e.trigger("dropdown_close",e.$dropdown)},positionDropdown:function(){var e=this.$control,t="body"===this.settings.dropdownParent?e.offset():e.position();t.top+=e.outerHeight(!0),this.$dropdown.css({width:e[0].getBoundingClientRect().width,top:t.top,left:t.left})},clear:function(e){var t=this;t.items.length&&(t.$control.children(":not(input)").remove(),t.items=[],t.lastQuery=null,t.setCaret(0),t.setActiveItem(null),t.updatePlaceholder(),t.updateOriginalInput({silent:e}),t.refreshState(),t.showInput(),t.trigger("clear"))},insertAtCaret:function(e){var t=Math.min(this.caretPos,this.items.length),n=e[0],r=this.buffer||this.$control[0];0===t?r.insertBefore(n,r.firstChild):r.insertBefore(n,r.childNodes[t]),this.setCaret(t+1)},deleteSelection:function(e){var t,n,r,i,o,a,s,l,u,c=this;if(r=e&&8===e.keyCode?-1:1,i=f(c.$control_input[0]),c.$activeOption&&!c.settings.hideSelected&&(s=c.getAdjacentOption(c.$activeOption,-1).attr("data-value")),o=[],c.$activeItems.length){for(u=c.$control.children(".active:"+(0
    '+e.title+'×
    '}},e),n.setup=(t=n.setup,function(){t.apply(n,arguments),n.$dropdown_header=S(e.html(e)),n.$dropdown.prepend(n.$dropdown_header)})}),w.define("optgroup_columns",function(s){var o,l=this;s=S.extend({equalizeWidth:!0,equalizeHeight:!0},s),this.getAdjacentOption=function(e,t){var n=e.closest("[data-group]").find("[data-selectable]"),r=n.index(e)+t;return 0<=r&&r
    ',e=e.firstChild,n.body.appendChild(e),t=u.width=e.offsetWidth-e.clientWidth,n.body.removeChild(e)),t},e=function(){var e,t,n,r,i,o,a;if((t=(a=S("[data-group]",l.$dropdown_content)).length)&&l.$dropdown_content.width()){if(s.equalizeHeight){for(e=n=0;e'+t.label+"",o.setup=(n=r.setup,function(){if(t.append){var i=r.settings.render.item;r.settings.render.item=function(e){return t=i.apply(o,arguments),n=a,r=t.search(/(<\/[^>]+>\s*)$/),t.substring(0,r)+n+t.substring(r);var t,n,r}}n.apply(o,arguments),o.$control.on("click","."+t.className,function(e){if(e.preventDefault(),!r.isLocked){var t=S(e.currentTarget).parent();r.setActiveItem(t),r.deleteSelection()&&r.setCaret(r.items.length)}})})):function(i,t){t.className="remove-single";var n,o=i,a=''+t.label+"";i.setup=(n=o.setup,function(){if(t.append){var e=S(o.$input.context).attr("id"),r=(S("#"+e),o.settings.render.item);o.settings.render.item=function(e){return t=r.apply(i,arguments),n=a,S("").append(t).append(n);var t,n}}n.apply(i,arguments),i.$control.on("click","."+t.className,function(e){e.preventDefault(),o.isLocked||o.clear()})})}(this,e)}),w.define("restore_on_backspace",function(r){var i,e=this;r.text=r.text||function(e){return e[this.settings.labelField]},this.onKeyDown=(i=e.onKeyDown,function(e){var t,n;return 8===e.keyCode&&""===this.$control_input.val()&&!this.$activeItems.length&&0<=(t=this.caretPos-1)&&t",{class:function(){var e=[];return e.push(i.options.state?"on":"off"),i.options.size&&e.push(i.options.size),i.options.disabled&&e.push("disabled"),i.options.readonly&&e.push("readonly"),i.options.indeterminate&&e.push("indeterminate"),i.options.inverse&&e.push("inverse"),i.$element.attr("id")&&e.push("id-"+i.$element.attr("id")),e.map(i._getClass.bind(i)).concat([i.options.baseClass],i._getClasses(i.options.wrapperClass)).join(" ")}}),this.$container=s("
    ",{class:this._getClass("container")}),this.$on=s("",{html:this.options.onText,class:this._getClass("handle-on")+" "+this._getClass(this.options.onColor)}),this.$off=s("",{html:this.options.offText,class:this._getClass("handle-off")+" "+this._getClass(this.options.offColor)}),this.$label=s("",{html:this.options.labelText,class:this._getClass("label")}),this.$element.on("init.bootstrapSwitch",this.options.onInit.bind(this,r)),this.$element.on("switchChange.bootstrapSwitch",function(){for(var e=arguments.length,t=Array(e),n=0;n-n._handleWidth/2;n._dragEnd=!1,n.state(n.options.inverse?!t:t)}else n.state(!n.options.state);n._dragStart=!1}},"mouseleave.bootstrapSwitch":function(){n.$label.trigger("mouseup.bootstrapSwitch")}})}},{key:"_externalLabelHandler",value:function(){var t=this,n=this.$element.closest("label");n.on("click",function(e){e.preventDefault(),e.stopImmediatePropagation(),e.target===n[0]&&t.toggleState()})}},{key:"_formHandler",value:function(){var e=this.$element.closest("form");e.data("bootstrap-switch")||e.on("reset.bootstrapSwitch",function(){window.setTimeout(function(){e.find("input").filter(function(){return s(this).data("bootstrap-switch")}).each(function(){return s(this).bootstrapSwitch("state",this.checked)})},1)}).data("bootstrap-switch",!0)}},{key:"_getClass",value:function(e){return this.options.baseClass+"-"+e}},{key:"_getClasses",value:function(e){return s.isArray(e)?e.map(this._getClass.bind(this)):[this._getClass(e)]}}]),t}();s.fn.bootstrapSwitch=function(o){for(var e=arguments.length,a=Array(1
    ');var r,i=f.overlay?"":" ngdialog-no-overlay";if((d=T('
    ')).html(f.overlay?'
    '+t+"
    ":'
    '+t+"
    "),d.data("$ngDialogOptions",f),c.ngDialogId=u,f.data&&O.isString(f.data)){var o=f.data.replace(/^\s*/,"")[0];c.ngDialogData="{"===o||"["===o?O.fromJson(f.data):new String(f.data),c.ngDialogData.ngDialogId=u}else f.data&&O.isObject(f.data)&&(c.ngDialogData=f.data,c.ngDialogData.ngDialogId=u);(f.className&&d.addClass(f.className),f.appendClassName&&d.addClass(f.appendClassName),f.width&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.width)?h.style.width=f.width:h.style.width=f.width+"px"),f.height&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.height)?h.style.height=f.height:h.style.height=f.height+"px"),f.disableAnimation&&d.addClass("ngdialog-disabled-animation"),p=f.appendTo&&O.isString(f.appendTo)?O.element(document.querySelector(f.appendTo)):b.body,$.applyAriaAttributes(d,f),f.preCloseCallback)&&(O.isFunction(f.preCloseCallback)?r=f.preCloseCallback:O.isString(f.preCloseCallback)&&c&&(O.isFunction(c[f.preCloseCallback])?r=c[f.preCloseCallback]:c.$parent&&O.isFunction(c.$parent[f.preCloseCallback])?r=c.$parent[f.preCloseCallback]:m&&O.isFunction(m[f.preCloseCallback])&&(r=m[f.preCloseCallback])),r&&d.data("$ngDialogPreCloseCallback",r));if(c.closeThisDialog=function(e){$.closeDialog(d,e)},f.controller&&(O.isString(f.controller)||O.isArray(f.controller)||O.isFunction(f.controller))){var a;f.controllerAs&&O.isString(f.controllerAs)&&(a=f.controllerAs);var s=w(f.controller,O.extend(n,{$scope:c,$element:d}),!0,a);f.bindToController&&O.extend(s.instance,{ngDialogId:c.ngDialogId,ngDialogData:c.ngDialogData,closeThisDialog:c.closeThisDialog,confirm:c.confirm}),"function"==typeof s?d.data("$ngDialogControllerController",s()):d.data("$ngDialogControllerController",s)}if(v(function(){var e=document.querySelectorAll(".ngdialog");$.deactivateAll(e),g(d)(c);var t=y.innerWidth-b.body.prop("clientWidth");b.html.addClass(f.bodyClassName),b.body.addClass(f.bodyClassName);var n=t-(y.innerWidth-b.body.prop("clientWidth"));0window.innerHeight&&(l=v,t++,e=0);var u=l?0===e?l:l+w:v,c=n+t*(b+s);o.css(o._positionY,u+"px"),"center"==o._positionX?o.css("left",parseInt(window.innerWidth/2-s/2)+"px"):o.css(o._positionX,c+"px"),r[o._positionY+o._positionX]=u+a,0m.maxCount&&0===i&&o.scope().kill(!0),e++}}},i=c(e)(n);i._positionY=h.positionY,i._positionX=h.positionX,i._priority=h.priority,i.addClass(h.type);var o=function(e){("click"===(e=e.originalEvent||e).type||"opacity"===e.propertyName&&1<=e.elapsedTime)&&(n.onClose&&n.$apply(n.onClose(i)),i.remove(),$.splice($.indexOf(i),1),n.$destroy(),r())};h.closeOnClick&&(i.addClass("clickable"),i.bind("click",o)),i.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd",o),angular.isNumber(h.delay)&&u(function(){i.addClass("killed")},h.delay),t("none"),angular.element(document.querySelector(h.container)).append(i);var a=-(parseInt(i[0].offsetHeight)+50);if(i.css(i._positionY,a+"px"),$.push(i),"center"==h.positionX){var s=parseInt(i[0].offsetWidth);i.css("left",parseInt(window.innerWidth/2-s/2)+"px")}u(function(){t("")}),n._templateElement=i,n.kill=function(e){e?(n.onClose&&n.$apply(n.onClose(n._templateElement)),$.splice($.indexOf(n._templateElement),1),n._templateElement.remove(),n.$destroy(),u(r)):n._templateElement.addClass("killed")},u(r),_||(angular.element(g).bind("resize",function(e){u(r)}),_=!0),l.resolve(n)}var l=a.defer();"object"==typeof h&&null!==h||(h={message:h}),h.scope=h.scope?h.scope:o,h.template=h.templateUrl?h.templateUrl:m.templateUrl,h.delay=angular.isUndefined(h.delay)?s:h.delay,h.type=e||h.type||m.type||"",h.positionY=h.positionY?h.positionY:m.positionY,h.positionX=h.positionX?h.positionX:m.positionX,h.replaceMessage=h.replaceMessage?h.replaceMessage:m.replaceMessage,h.onClose=h.onClose?h.onClose:m.onClose,h.closeOnClick=null!==h.closeOnClick&&void 0!==h.closeOnClick?h.closeOnClick:m.closeOnClick,h.container=h.container?h.container:m.container,h.priority=h.priority?h.priority:m.priority;var n=i.get(h.template);return n?t(n):r.get(h.template,{cache:!0}).then(function(e){t(e.data)}).catch(function(e){throw new Error("Template ("+h.template+") could not be loaded. "+e)}),l.promise};return t.primary=function(e){return this(e,"primary")},t.error=function(e){return this(e,"error")},t.success=function(e){return this(e,"success")},t.info=function(e){return this(e,"info")},t.warning=function(e){return this(e,"warning")},t.clearAll=function(){angular.forEach($,function(e){e.addClass("killed")})},t}]}),angular.module("ui-notification").run(["$templateCache",function(e){e.put("angular-ui-notification.html",'

    ')}]),function(){var w="__default";angular.module("angularUtils.directives.dirPagination",[]).directive("dirPaginate",["$compile","$parse","paginationService",function(m,v,y){return{terminal:!0,multiElement:!0,priority:100,compile:function(e,t){var f=t.dirPaginate,n=f.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),r=/\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/;if(null===n[2].match(r))throw"pagination directive: the 'itemsPerPage' filter must be set.";var i=n[2].replace(r,""),g=v(i);o=e,angular.forEach(o,function(e){1===e.nodeType&&angular.element(e).attr("dir-paginate-no-compile",!0)});var o;var a=t.paginationId||w;return y.registerInstance(a),function(e,t,n){var r=v(n.paginationId)(e)||n.paginationId||w;y.registerInstance(r);var i,o,a,s,l,u,c,d=(u=r,c=!!(l=f).match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/),u===w||c?l:l.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/,"$1 : '"+u+"'"));o=n,a=d,(i=t)[0].hasAttribute("dir-paginate-start")||i[0].hasAttribute("data-dir-paginate-start")?(o.$set("ngRepeatStart",a),i.eq(i.length-1).attr("ng-repeat-end",!0)):o.$set("ngRepeat",a),s=t,angular.forEach(s,function(e){1===e.nodeType&&angular.element(e).removeAttr("dir-paginate-no-compile")}),s.eq(0).removeAttr("dir-paginate-start").removeAttr("dir-paginate").removeAttr("data-dir-paginate-start").removeAttr("data-dir-paginate"),s.eq(s.length-1).removeAttr("dir-paginate-end").removeAttr("data-dir-paginate-end");var p=m(t),h=function(e,t,n){var r;if(t.currentPage)r=v(t.currentPage);else{var i=(n+"__currentPage").replace(/\W/g,"_");e[i]=1,r=v(i)}return r}(e,n,r);y.setCurrentPageParser(r,h,e),void 0!==n.totalItems?(y.setAsyncModeTrue(r),e.$watch(function(){return v(n.totalItems)(e)},function(e){0<=e&&y.setCollectionLength(r,e)})):(y.setAsyncModeFalse(r),e.$watchCollection(function(){return g(e)},function(e){if(e){var t=e instanceof Array?e.length:Object.keys(e).length;y.setCollectionLength(r,t)}})),p(e)}}}}]).directive("dirPaginateNoCompile",function(){return{priority:5e3,terminal:!0}}).directive("dirPaginationControls",["paginationService","paginationTemplate",function(d,n){var p=/^\d+$/,e={restrict:"AE",scope:{maxSize:"=?",onPageChange:"&?",paginationId:"=?",autoHide:"=?"},link:function(r,e,t){var n=t.paginationId||w,i=r.paginationId||t.paginationId||w;if(!d.isRegistered(i)&&!d.isRegistered(n)){var o=i!==w?" (id: "+i+") ":" ";window.console&&console.warn("Pagination directive: the pagination controls"+o+"cannot be used without the corresponding pagination directive, which was not found at link time.")}r.maxSize||(r.maxSize=9);r.autoHide=void 0===r.autoHide||r.autoHide,r.directionLinks=!angular.isDefined(t.directionLinks)||r.$parent.$eval(t.directionLinks),r.boundaryLinks=!!angular.isDefined(t.boundaryLinks)&&r.$parent.$eval(t.boundaryLinks);var a=Math.max(r.maxSize,5);function s(e){if(d.isRegistered(i)&&c(e)){var t=r.pagination.current;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,u(),r.onPageChange&&r.onPageChange({newPageNumber:e,oldPageNumber:t})}}function l(){if(d.isRegistered(i)){var e=parseInt(d.getCurrentPage(i))||1;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,r.pagination.last=r.pages[r.pages.length-1],r.pagination.last
  • «
  • {{ pageNumber }}
  • »
  • ')}])}();var com_github_culmat_jsTreeTable=function(){function l(e,r,i){return i=i||"children",$.each(e,function(e,t){!function n(e){e[i]&&$.each(e[i],function(e,t){n(t)}),r(e)}(t)}),e}function t(e,n,o,a){var t=e;n=n||"id",o=o||"parent",a=a||"children";var s=[];$.each(t,function(e,t){s[t[n]]=t});var l=[];return $.each(t,function(e,r){var t=r[o];if($.isArray(t)||(t=[t]),0==t.length)l.push(r);else{var i=!1;$.each(t,function(e,t){var n=s[t];n&&(n[a]||(n[a]=[]),$.inArray(r,n[a])<0&&n[a].push(r),i=!0)}),i||l.push(r)}}),l}function u(e,u,c,d,p,t){u=u||"children",c=c||"id",t=t||{};var n=0,r=$("");$.each(t,function(e,t){"class"==e&&"jsTT"!=t?r.addClass(t):r.attr(e,t)});var i=$(""),o=$(""),h=$("");return r.append(i),i.append(o),r.append(h),d?$.each(d,function(e,t){$(o).append($(""))}):($(o).append($("")),$.each(e[0],function(e,t){e!=u&&e!=c&&$(o).append($(""))})),function o(e,a,s,l){n=Math.max(n,s),$.each(e,function(e,n){var r,t,i;n["data-tt-level"]=s,r=n,t=l,i=$(""),$(i).attr("data-tt-id",r[c]),$(i).attr("data-tt-level",r["data-tt-level"]),r[u]&&0!=r[u].length?$(i).attr("data-tt-isnode",!0):$(i).attr("data-tt-isleaf",!0),t&&$(i).attr("data-tt-parent-id",t[c]),p?p($(i),r):d?$.each(d,function(e,t){$(i).append($(""))}):($(i).append($("")),$.each(r,function(e,t){e!=u&&e!=c&&"data-tt-level"!=e&&$(i).append($(""))})),h.append(i),n[a]&&$.each(n[a],function(e,t){o([t],a,s+1,n)})})}(e,u,1),e[0]&&(e[0].maxLevel=n),r}function n(e,t){return $.each(e,function(e,n){$.each(t,function(e,t){n[t]=$(n).attr(t)})}),e}function c(i){i.addClass("jsTT"),i.expandLevel=function(n){$("tr[data-tt-level]",i).each(function(e){var t=parseInt($(this).attr("data-tt-level"));n-1')):r.prepend($('')),r.prepend($('')),t.trExpand=function(e){if(!(this.trChildren.length<1)){e&&(this.trChildrenVisible=!0,$("#state",this).get(0).src=o);var n=e||this.trChildrenVisible;$.each(this.trChildren,function(e,t){n&&$(t).css("display","table-row"),t.trExpand()})}},t.trCollapse=function(e){this.trChildren.length<1||(e&&(this.trChildrenVisible=!1,$("#state",this).get(0).src="data:image/png;base64,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"),$.each(this.trChildren,function(e,t){$(t).css("display","none"),t.trCollapse()}))},$(t).click(function(){this.trChildrenVisible?this.trCollapse(!0):this.trExpand(!0)})}),i}return{depthFirst:l,makeTree:t,renderTree:u,attr2attr:n,treeTable:c,appendTreetable:function(e,t){(t=t||{}).idAttr=t.idAttr||"id",t.childrenAttr=t.childrenAttr||"children";var n=t.controls||[];t.mountPoint||(t.mountPoint=$("body")),t.depthFirst&&l(e,t.depthFirst,t.childrenAttr);var r=u(e,t.childrenAttr,t.idAttr,t.renderedAttr,t.renderer,t.tableAttributes);c(r),t.replaceContent&&t.mountPoint.html("");var i,o,a=t.initialExpandLevel?parseInt(t.initialExpandLevel):-1;if(a=Math.min(a,e[0].maxLevel),r.expandLevel(a),t.slider){var s=$('
    ');s.width("200px"),s.slider({min:1,max:e[0].maxLevel,range:"min",value:a,slide:function(e,t){r.expandLevel(t.value)}}),n=[s].concat(t.controls)}return 0"),$.each(i,function(e,t){o.append($('
    "+t+""+c+""+e+"
    "+r[e]+""+r[c]+""+t+"').append(t))}),$('').append(o))),t.mountPoint.append(r),r},jsTreeTable:"1.0",register:function(n){$.each(this,function(e,t){"register"!=e&&(n[e]=t)})}}}(); \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/gulpfile.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/gulpfile.js new file mode 100644 index 0000000..06ea3fc --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/gulpfile.js @@ -0,0 +1,134 @@ +const gulp = require('gulp'); +const plugins = require('gulp-load-plugins')(); +const open = require('open'); +const app = { + srcPath: 'app/', // 源代码 + devPath: 'tmp/', // 开发打包 + prdPath: 'dist/' // 生产打包 +}; + +const JS_LIBS = [ + 'node_modules/angular-ui-router/release/angular-ui-router.js', + 'node_modules/oclazyload/dist/ocLazyLoad.min.js', + 'node_modules/angular-loading-bar/build/loading-bar.min.js', + 'node_modules/angular-bootstrap/ui-bootstrap-tpls.min.js', + 'node_modules/moment/moment.js', + 'node_modules/angular-date-time-input/src/dateTimeInput.js', + 'node_modules/angularjs-bootstrap-datetimepicker/src/js/datetimepicker.js', + 'node_modules/angular-table-resize/dist/angular-table-resize.min.js', + 'node_modules/angular-clipboard/angular-clipboard.js', + 'node_modules/selectize/dist/js/standalone/selectize.js', + 'node_modules/angular-selectize2/dist/selectize.js', + 'node_modules/bootstrap-switch/dist/js/bootstrap-switch.min.js', + 'node_modules/ng-dialog/js/ngDialog.js', + 'node_modules/angular-ui-notification/dist/angular-ui-notification.min.js', + 'node_modules/angular-utils-pagination/dirPagination.js', + 'app/scripts/libs/treeTable.js', +]; + +const CSS_APP = [ + 'node_modules/angular-loading-bar/build/loading-bar.min.css', + 'node_modules/bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css', + 'node_modules/ng-dialog/css/ngDialog.min.css', + 'node_modules/ng-dialog/css/ngDialog-theme-default.css', + 'node_modules/angularjs-bootstrap-datetimepicker/src/css/datetimepicker.css', + 'node_modules/angular-ui-notification/dist/angular-ui-notification.min.css', + 'node_modules/angular-table-resize/dist/angular-table-resize.css', + 'node_modules/selectize/dist/css/selectize.css', + 'app/styles/page.css', + 'app/styles/timeline.css', + 'app/styles/main.css' +]; + +const JS_APP = [ + 'app/scripts/app.js', + 'app/scripts/filters/filters.js', + 'app/scripts/services/version_service.js', + 'app/scripts/services/auth_service.js', + 'app/scripts/services/appservice.js', + 'app/scripts/services/flow_service_v1.js', + 'app/scripts/services/flow_service_v2.js', + 'app/scripts/services/degrade_service.js', + 'app/scripts/services/systemservice.js', + 'app/scripts/services/machineservice.js', + 'app/scripts/services/identityservice.js', + 'app/scripts/services/metricservice.js', + 'app/scripts/services/param_flow_service.js', + 'app/scripts/services/authority_service.js', + 'app/scripts/services/cluster_state_service.js', + 'app/scripts/services/gateway/api_service.js', + 'app/scripts/services/gateway/flow_service.js', +]; + +gulp.task('lib', function () { + gulp.src(JS_LIBS) + .pipe(plugins.concat('app.vendor.js')) + .pipe(gulp.dest(app.devPath + 'js')) + .pipe(plugins.uglify()) + .pipe(gulp.dest(app.prdPath + 'js')) + .pipe(plugins.connect.reload()); +}); + +/* +* css任务 +* 在src下创建style文件夹,里面存放less文件。 +*/ +gulp.task('css', function () { + gulp.src(CSS_APP) + .pipe(plugins.concat('app.css')) + .pipe(gulp.dest(app.devPath + 'css')) + .pipe(plugins.cssmin()) + .pipe(gulp.dest(app.prdPath + 'css')) + .pipe(plugins.connect.reload()); +}); + +/* +* js任务 +* 在src目录下创建script文件夹,里面存放所有的js文件 +*/ +gulp.task('js', function () { + gulp.src(JS_APP) + .pipe(plugins.concat('app.js')) + .pipe(gulp.dest(app.devPath + 'js')) + .pipe(plugins.uglify()) + .pipe(gulp.dest(app.prdPath + 'js')) + .pipe(plugins.connect.reload()); +}); + +/* +* js任务 +* 在src目录下创建script文件夹,里面存放所有的js文件 +*/ +gulp.task('jshint', function () { + gulp.src(JS_APP) + .pipe(plugins.jshint()) + .pipe(plugins.jshint.reporter()); +}); + +// 每次发布的时候,可能需要把之前目录内的内容清除,避免旧的文件对新的容有所影响。 需要在每次发布前删除dist和build目录 +gulp.task('clean', function () { + gulp.src([app.devPath, app.prdPath]) + .pipe(plugins.clean()); +}); + +// 总任务 +gulp.task('build', ['clean', 'jshint', 'lib', 'js', 'css']); + +// 服务 +gulp.task('serve', ['build'], function () { + plugins.connect.server({ //启动一个服务器 + root: [app.devPath], // 服务器从哪个路径开始读取,默认从开发路径读取 + livereload: true, // 自动刷新 + port: 1234 + }); + // 打开浏览器 + setTimeout(() => { + open('http://localhost:8080/index_dev.htm') + }, 200); + // 监听 + gulp.watch(app.srcPath + '**/*.js', ['js']); + gulp.watch(app.srcPath + '**/*.css', ['css']); +}); + +// 定义default任务 +gulp.task('default', ['serve']); diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/index.htm b/chushang-visual/chushang-sentinel/src/main/webapp/resources/index.htm new file mode 100644 index 0000000..3c83341 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/index.htm @@ -0,0 +1,28 @@ + + + + + + Sentinel Dashboard + + + + + + + + + +
    +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/index_dev.htm b/chushang-visual/chushang-sentinel/src/main/webapp/resources/index_dev.htm new file mode 100644 index 0000000..a2a85ef --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/index_dev.htm @@ -0,0 +1,28 @@ + + + + + + Sentinel 控制台 + + + + + + + + + +
    +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/bootstrap.min.css b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/bootstrap.min.css new file mode 100644 index 0000000..c547283 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.0.3 (http://getbootstrap.com) + * Copyright 2013 Twitter, Inc. + * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + */ + +/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#8a6d3b}.text-warning:hover{color:#66512c}.text-danger{color:#a94442}.text-danger:hover{color:#843534}.text-success{color:#3c763d}.text-success:hover{color:#2b542c}.text-info{color:#31708f}.text-info:hover{color:#245269}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#dff0d8}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#d0e9c6}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#f2dede}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#ebcccc}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#fff}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}} \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/font-awesome.min.css b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/font-awesome.min.css new file mode 100644 index 0000000..540440c --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/fonts/fontawesome-webfont.ttf b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..35acda2fa1196aad98c2adf4378a7611dd713aa3 GIT binary patch literal 165548 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 z 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 r*").append(a).html();try{return a[0].nodeType===Na?F(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+F(b)})}catch(c){return F(d)}}function wc(a){try{return decodeURIComponent(a)}catch(b){}} +function xc(a){var b={};n((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=wc(e),y(e)&&(f=y(f)?wc(f):!0,qa.call(b,e)?I(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Qb(a){var b=[];n(a,function(a,c){I(a)?n(a,function(a){b.push(ja(c,!0)+(!0===a?"":"="+ja(a,!0)))}):b.push(ja(c,!0)+(!0===a?"":"="+ja(a,!0)))});return b.length?b.join("&"):""}function ob(a){return ja(a,!0).replace(/%26/gi,"&").replace(/%3D/gi, +"=").replace(/%2B/gi,"+")}function ja(a,b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function Yd(a,b){var d,c,e=Oa.length;for(c=0;c/,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=eb(b,d.strictDi);c.invoke(["$rootScope", +"$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;S&&e.test(S.name)&&(d.debugInfoEnabled=!0,S.name=S.name.replace(e,""));if(S&&!f.test(S.name))return c();S.name=S.name.replace(f,"");fa.resumeBootstrap=function(a){n(a,function(a){b.push(a)});return c()};z(fa.resumeDeferredBootstrap)&&fa.resumeDeferredBootstrap()}function $d(){S.name="NG_ENABLE_DEBUG_INFO!"+S.name;S.location.reload()} +function ae(a){a=fa.element(a).injector();if(!a)throw Aa("test");return a.get("$$testability")}function zc(a,b){b=b||"_";return a.replace(be,function(a,c){return(c?b:"")+a.toLowerCase()})}function ce(){var a;if(!Ac){var b=pb();(oa=q(b)?S.jQuery:b?S[b]:u)&&oa.fn.on?(B=oa,M(oa.fn,{scope:Pa.scope,isolateScope:Pa.isolateScope,controller:Pa.controller,injector:Pa.injector,inheritedData:Pa.inheritedData}),a=oa.cleanData,oa.cleanData=function(b){var c;if(Rb)Rb=!1;else for(var e=0,f;null!=(f=b[e]);e++)(c= +oa._data(f,"events"))&&c.$destroy&&oa(f).triggerHandler("$destroy");a(b)}):B=N;fa.element=B;Ac=!0}}function qb(a,b,d){if(!a)throw Aa("areq",b||"?",d||"required");return a}function Qa(a,b,d){d&&I(a)&&(a=a[a.length-1]);qb(z(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ra(a,b){if("hasOwnProperty"===a)throw Aa("badname",b);}function Bc(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g")+c[2];for(c=c[0];c--;)d=d.lastChild;f=cb(f,d.childNodes);d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";n(f,function(a){e.appendChild(a)});return e}function N(a){if(a instanceof N)return a;var b;E(a)&&(a=U(a), +b=!0);if(!(this instanceof N)){if(b&&"<"!=a.charAt(0))throw Ub("nosel");return new N(a)}if(b){b=X;var d;a=(d=Ef.exec(a))?[b.createElement(d[1])]:(d=Lc(a,b))?d.childNodes:[]}Mc(this,a)}function Vb(a){return a.cloneNode(!0)}function ub(a,b){b||vb(a);if(a.querySelectorAll)for(var d=a.querySelectorAll("*"),c=0,e=d.length;cl&&this.remove(t.key);return b}},get:function(a){if(l").parent()[0])});var f=O(a,b,a,c,d,e);K.$$addScopeClass(a);var g=null;return function(b,c,d){qb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d= +d&&d[0])?"foreignobject"!==ta(d)&&d.toString().match(/SVG/)?"svg":"html":"html");d="html"!==g?B(Yb(g,B("

    ").append(a).html())):c?Pa.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);K.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);return d}}function O(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,t,w,D;if(p)for(D=Array(c.length),m=0;mq.priority)break;if(P=q.scope)q.templateUrl||(H(P)?(Ua("new/isolated scope",O||R,q,Z),O=q):Ua("new/isolated scope",O,q,Z)),R=R||q;x=q.name;!q.templateUrl&&q.controller&&(P=q.controller,T=T||$(),Ua("'"+x+"' controller",T[x],q,Z),T[x]=q);if(P=q.transclude)ga=!0,q.$$tlb||(Ua("transclusion",n,q,Z),n=q),"element"==P?(aa=!0,A=q.priority,P=Z,Z=d.$$element=B(X.createComment(" "+x+": "+d[x]+" ")),b=Z[0],Y(f,ra.call(P,0), +b),Ia=K(P,e,A,g&&g.name,{nonTlbTranscludeDirective:n})):(P=B(Vb(b)).contents(),Z.empty(),Ia=K(P,e,u,u,{needsNewScope:q.$$isolateScope||q.$$newScope}));if(q.template)if(L=!0,Ua("template",J,q,Z),J=q,P=z(q.template)?q.template(Z,d):q.template,P=ja(P),q.replace){g=q;P=Tb.test(P)?Xc(Yb(q.templateNamespace,U(P))):[];b=P[0];if(1!=P.length||1!==b.nodeType)throw ha("tplrt",x,"");Y(f,Z,b);P={$attr:{}};var Wc=V(b,[],P),W=a.splice(F+1,a.length-(F+1));(O||R)&&y(Wc,O,R);a=a.concat(Wc).concat(W);S(d,P);M=a.length}else Z.html(P); +if(q.templateUrl)L=!0,Ua("template",J,q,Z),J=q,q.replace&&(g=q),D=Of(a.splice(F,a.length-F),Z,d,f,ga&&Ia,h,l,{controllerDirectives:T,newScopeDirective:R!==q&&R,newIsolateScopeDirective:O,templateDirective:J,nonTlbTranscludeDirective:n}),M=a.length;else if(q.compile)try{G=q.compile(Z,d,Ia),z(G)?t(null,G,N,Q):G&&t(G.pre,G.post,N,Q)}catch(da){c(da,ua(Z))}q.terminal&&(D.terminal=!0,A=Math.max(A,q.priority))}D.scope=R&&!0===R.scope;D.transcludeOnThisElement=ga;D.templateOnThisElement=L;D.transclude=Ia; +m.hasElementTranscludeDirective=aa;return D}function y(a,b,c){for(var d=0,e=a.length;dm.priority)&&-1!=m.restrict.indexOf(f)&&(k&&(m=Ob(m,{$$start:k,$$end:l})),b.push(m),h=m)}catch(D){c(D)}}return h}function G(b){if(e.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,f=c.length;d"+b+"";return c.childNodes[0].childNodes;default:return b}}function Q(a,b){if("srcdoc"==b)return L.HTML;var c=ta(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return L.RESOURCE_URL}function W(a,c,d,e,f){var g=Q(a,e);f=h[e]||f;var k=b(d,!0,g,f);if(k){if("multiple"===e&&"select"===ta(a))throw ha("selmulti",ua(a));c.push({priority:100,compile:function(){return{pre:function(a,c,h){c=h.$$observers||(h.$$observers=$());if(l.test(e))throw ha("nodomevents"); +var m=h[e];m!==d&&(k=m&&b(m,!0,g,f),d=m);k&&(h[e]=k(a),(c[e]||(c[e]=[])).$$inter=!0,(h.$$observers&&h.$$observers[e].$$scope||a).$watch(k,function(a,b){"class"===e&&a!=b?h.$updateClass(a,b):h.$set(e,a)}))}}}})}}function Y(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;)8===a[b].nodeType&&Pf.call(a,b,1);return a}function Xe(){var a={},b=!1;this.register=function(b,c){Ra(b,"controller");H(b)?M(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector","$window",function(d,c){function e(a,b,c,d){if(!a||!H(a.$scope))throw G("$controller")("noscp", +d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,r;h=!0===h;k&&E(k)&&(r=k);if(E(f)){k=f.match(Uc);if(!k)throw Qf("ctrlfmt",f);m=k[1];r=r||k[3];f=a.hasOwnProperty(m)?a[m]:Bc(g.$scope,m,!0)||(b?Bc(c,m,!0):u);Qa(f,m,!0)}if(h)return h=(I(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),r&&e(g,r,l,m||f.name),M(function(){var a=d.invoke(f,l,g,m);a!==l&&(H(a)||z(a))&&(l=a,r&&e(g,r,l,m||f.name));return l},{instance:l,identifier:r});l=d.instantiate(f,g,m);r&&e(g,r,l,m||f.name);return l}}]}function Ye(){this.$get= +["$window",function(a){return B(a.document)}]}function Ze(){this.$get=["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function Zb(a){return H(a)?da(a)?a.toISOString():db(a):a}function df(){this.$get=function(){return function(a){if(!a)return"";var b=[];oc(a,function(a,c){null===a||q(a)||(I(a)?n(a,function(a,d){b.push(ja(c)+"="+ja(Zb(a)))}):b.push(ja(c)+"="+ja(Zb(a))))});return b.join("&")}}}function ef(){this.$get=function(){return function(a){function b(a,e,f){null===a||q(a)|| +(I(a)?n(a,function(a,c){b(a,e+"["+(H(a)?c:"")+"]")}):H(a)&&!da(a)?oc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):d.push(ja(e)+"="+ja(Zb(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function $b(a,b){if(E(a)){var d=a.replace(Rf,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf($c))||(c=(c=d.match(Sf))&&Tf[c[0]].test(d));c&&(a=uc(d))}}return a}function ad(a){var b=$(),d;E(a)?n(a.split("\n"),function(a){d=a.indexOf(":");var e=F(U(a.substr(0,d)));a=U(a.substr(d+1));e&& +(b[e]=b[e]?b[e]+", "+a:a)}):H(a)&&n(a,function(a,d){var f=F(d),g=U(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function bd(a){var b;return function(d){b||(b=ad(a));return d?(d=b[F(d)],void 0===d&&(d=null),d):b}}function cd(a,b,d,c){if(z(c))return c(a,b,d);n(c,function(c){a=c(a,b,d)});return a}function cf(){var a=this.defaults={transformResponse:[$b],transformRequest:[function(a){return H(a)&&"[object File]"!==sa.call(a)&&"[object Blob]"!==sa.call(a)&&"[object FormData]"!==sa.call(a)?db(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"}, +post:ia(ac),put:ia(ac),patch:ia(ac)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer"},b=!1;this.useApplyAsync=function(a){return y(a)?(b=!!a,this):b};var d=!0;this.useLegacyPromiseExtensions=function(a){return y(a)?(d=!!a,this):d};var c=this.interceptors=[];this.$get=["$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector",function(e,f,g,h,k,l){function m(b){function c(a){var b=M({},a);b.data=cd(a.data,a.headers,a.status,f.transformResponse); +a=a.status;return 200<=a&&300>a?b:k.reject(b)}function e(a,b){var c,d={};n(a,function(a,e){z(a)?(c=a(b),null!=c&&(d[e]=c)):d[e]=a});return d}if(!fa.isObject(b))throw G("$http")("badreq",b);var f=M({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer},b);f.headers=function(b){var c=a.headers,d=M({},b.headers),f,g,h,c=M({},c.common,c[F(b.method)]);a:for(f in c){g=F(f);for(h in d)if(F(h)===g)continue a;d[f]=c[f]}return e(d,ia(b))}(b); +f.method=sb(f.method);f.paramSerializer=E(f.paramSerializer)?l.get(f.paramSerializer):f.paramSerializer;var g=[function(b){var d=b.headers,e=cd(b.data,bd(d),u,b.transformRequest);q(e)&&n(d,function(a,b){"content-type"===F(b)&&delete d[b]});q(b.withCredentials)&&!q(a.withCredentials)&&(b.withCredentials=a.withCredentials);return r(b,e).then(c,c)},u],h=k.when(f);for(n(v,function(a){(a.request||a.requestError)&&g.unshift(a.request,a.requestError);(a.response||a.responseError)&&g.push(a.response,a.responseError)});g.length;){b= +g.shift();var m=g.shift(),h=h.then(b,m)}d?(h.success=function(a){Qa(a,"fn");h.then(function(b){a(b.data,b.status,b.headers,f)});return h},h.error=function(a){Qa(a,"fn");h.then(null,function(b){a(b.data,b.status,b.headers,f)});return h}):(h.success=dd("success"),h.error=dd("error"));return h}function r(c,d){function g(a,c,d,e){function f(){l(c,a,d,e)}J&&(200<=a&&300>a?J.put(R,[a,c,ad(d),e]):J.remove(R));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function l(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?n.resolve: +n.reject)({data:a,status:b,headers:bd(d),config:c,statusText:e})}function r(a){l(a.data,a.status,ia(a.headers()),a.statusText)}function v(){var a=m.pendingRequests.indexOf(c);-1!==a&&m.pendingRequests.splice(a,1)}var n=k.defer(),D=n.promise,J,K,O=c.headers,R=t(c.url,c.paramSerializer(c.params));m.pendingRequests.push(c);D.then(v,v);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(J=H(c.cache)?c.cache:H(a.cache)?a.cache:A);J&&(K=J.get(R),y(K)?K&&z(K.then)?K.then(r,r):I(K)?l(K[1], +K[0],ia(K[2]),K[3]):l(K,200,{},"OK"):J.put(R,D));q(K)&&((K=ed(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:u)&&(O[c.xsrfHeaderName||a.xsrfHeaderName]=K),e(c.method,R,d,g,O,c.timeout,c.withCredentials,c.responseType));return D}function t(a,b){0=k&&(p.resolve(v),A(C.$$intervalId),delete f[C.$$intervalId]);n||a.$apply()},h);f[C.$$intervalId]=p;return C}var f={};e.cancel=function(a){return a&&a.$$intervalId in f?(f[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),delete f[a.$$intervalId],!0):!1};return e}]}function bc(a){a=a.split("/");for(var b=a.length;b--;)a[b]=ob(a[b]);return a.join("/")}function fd(a,b){var d=wa(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=ea(d.port)||Vf[d.protocol]|| +null}function gd(a,b){var d="/"!==a.charAt(0);d&&(a="/"+a);var c=wa(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=xc(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!=b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function pa(a,b){if(0===b.indexOf(a))return b.substr(a.length)}function Fa(a){var b=a.indexOf("#");return-1==b?a:a.substr(0,b)}function ib(a){return a.replace(/(#.+)|#$/,"$1")}function cc(a,b,d){this.$$html5=!0;d=d||""; +fd(a,this);this.$$parse=function(a){var d=pa(b,a);if(!E(d))throw Db("ipthprfx",a,b);gd(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Qb(this.$$search),d=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(a?"?"+a:"")+d;this.$$absUrl=b+this.$$url.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;y(f=pa(a,c))?(g=f,g=y(f=pa(d,f))?b+(pa("/",f)||f):a+g):y(f=pa(b,c))?g=b+f:b==c+"/"&&(g=b);g&&this.$$parse(g); +return!!g}}function dc(a,b,d){fd(a,this);this.$$parse=function(c){var e=pa(a,c)||pa(b,c),f;q(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",q(e)&&(a=c,this.replace())):(f=pa(d,e),q(f)&&(f=e));gd(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;0===f.indexOf(e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Qb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url? +d+this.$$url:"")};this.$$parseLinkUrl=function(b,d){return Fa(a)==Fa(b)?(this.$$parse(b),!0):!1}}function hd(a,b,d){this.$$html5=!0;dc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a==Fa(c)?f=c:(g=pa(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Qb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url}}function Eb(a){return function(){return this[a]}} +function id(a,b){return function(d){if(q(d))return this[a];this[a]=b(d);this.$$compose();return this}}function hf(){var a="",b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return y(b)?(a=b,this):a};this.html5Mode=function(a){return $a(a)?(b.enabled=a,this):H(a)?($a(a.enabled)&&(b.enabled=a.enabled),$a(a.requireBase)&&(b.requireBase=a.requireBase),$a(a.rewriteLinks)&&(b.rewriteLinks=a.rewriteLinks),this):b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window", +function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state=f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,b)}var l,m;m=c.baseHref();var r=c.url(),t;if(b.enabled){if(!m&&b.requireBase)throw Db("nobase");t=r.substring(0,r.indexOf("/",r.indexOf("//")+2))+(m||"/");m=e.history?cc:hd}else t=Fa(r),m=dc;var A=t.substr(0,Fa(t).lastIndexOf("/")+1);l=new m(t,A,"#"+a);l.$$parseLinkUrl(r,r);l.$$state= +c.state();var v=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(b.rewriteLinks&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!=a.which&&2!=a.button){for(var e=B(a.target);"a"!==ta(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return;var h=e.prop("href"),k=e.attr("href")||e.attr("xlink:href");H(h)&&"[object SVGAnimatedString]"===h.toString()&&(h=wa(h.animVal).href);v.test(h)||!h||e.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(h,k)||(a.preventDefault(),l.absUrl()!=c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]= +!0))}});ib(l.absUrl())!=ib(r)&&c.url(l.absUrl(),!0);var n=!0;c.onUrlChange(function(a,b){q(pa(A,a))?g.location.href=a:(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=ib(a);l.$$parse(a);l.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(n=!1,k(c,e)))}),d.$$phase||d.$digest())});d.$watch(function(){var a=ib(c.url()),b=ib(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(n|| +m)n=!1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:l.$$state),k(a,f)))});l.$$replace=!1});return l}]}function jf(){var a=!0,b=this;this.debugEnabled=function(b){return y(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&& +(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||x;a=!1;try{a=!!e.apply}catch(k){}return a?function(){var a=[];n(arguments,function(b){a.push(c(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Va(a,b){if("__defineGetter__"===a||"__defineSetter__"===a||"__lookupGetter__"===a||"__lookupSetter__"=== +a||"__proto__"===a)throw ba("isecfld",b);return a}function jd(a,b){a+="";if(!E(a))throw ba("iseccst",b);return a}function xa(a,b){if(a){if(a.constructor===a)throw ba("isecfn",b);if(a.window===a)throw ba("isecwindow",b);if(a.children&&(a.nodeName||a.prop&&a.attr&&a.find))throw ba("isecdom",b);if(a===Object)throw ba("isecobj",b);}return a}function kd(a,b){if(a){if(a.constructor===a)throw ba("isecfn",b);if(a===Wf||a===Xf||a===Yf)throw ba("isecff",b);}}function ld(a,b){if(a&&(a===(0).constructor||a=== +(!1).constructor||a==="".constructor||a==={}.constructor||a===[].constructor||a===Function.constructor))throw ba("isecaf",b);}function Zf(a,b){return"undefined"!==typeof a?a:b}function md(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function W(a,b){var d,c;switch(a.type){case s.Program:d=!0;n(a.body,function(a){W(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:W(a.argument,b);a.constant=a.argument.constant; +a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:W(a.left,b);W(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:W(a.left,b);W(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:W(a.test,b);W(a.alternate,b);W(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant= +!1;a.toWatch=[a];break;case s.MemberExpression:W(a.object,b);a.computed&&W(a.property,b);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=a.filter?!b(a.callee.name).$stateful:!1;c=[];n(a.arguments,function(a){W(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=a.filter&&!b(a.callee.name).$stateful?c:[a];break;case s.AssignmentExpression:W(a.left,b);W(a.right,b);a.constant=a.left.constant&&a.right.constant; +a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];n(a.elements,function(a){W(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=c;break;case s.ObjectExpression:d=!0;c=[];n(a.properties,function(a){W(a.value,b);d=d&&a.value.constant;a.value.constant||c.push.apply(c,a.value.toWatch)});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1,a.toWatch=[]}}function nd(a){if(1==a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:u}} +function od(a){return a.type===s.Identifier||a.type===s.MemberExpression}function pd(a){if(1===a.body.length&&od(a.body[0].expression))return{type:s.AssignmentExpression,left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function qd(a){return 0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression)}function rd(a,b){this.astBuilder=a;this.$filter=b}function sd(a, +b){this.astBuilder=a;this.$filter=b}function Fb(a){return"constructor"==a}function ec(a){return z(a.valueOf)?a.valueOf():$f.call(a)}function kf(){var a=$(),b=$();this.$get=["$filter",function(d){function c(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=ec(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function e(a,b,d,e,f){var g=e.inputs,h;if(1===g.length){var k=c,g=g[0];return a.$watch(function(a){var b=g(a);c(b,k)||(h=e(a,u,u,[b]),k=b&&ec(b));return h},b,d,f)}for(var l=[],m=[],r=0,n= +g.length;r=this.promise.$$state.status&&d&&d.length&&a(function(){for(var a,e,f=0,g=d.length;fa)for(b in l++,f)qa.call(e,b)||(n--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,g,k=1n&&(v=4-n,q[v]||(q[v]=[]),q[v].push({msg:z(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:f,oldVal:h}));else if(a===c){r=!1;break a}}catch(y){g(y)}if(!(l=A.$$watchersCount&&A.$$childHead||A!==this&&A.$$nextSibling))for(;A!==this&&!(l=A.$$nextSibling);)A=A.$parent}while(A=l);if((r||u.length)&&!n--)throw w.$$phase=null,d("infdig", +b,q);}while(r||u.length);for(w.$$phase=null;L.length;)try{L.shift()()}catch(x){g(x)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===w&&k.$$applicationDestroyed();A(this,-this.$$watchersCount);for(var b in this.$$listenerCount)v(this,this.$$listenerCount[b],b);a&&a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling= +this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=x;this.$on=this.$watch=this.$watchGroup=function(){return x};this.$$listeners={};this.$$nextSibling=null;m(this)}},$eval:function(a,b){return h(a)(this,b)},$evalAsync:function(a,b){w.$$phase||u.length||k.defer(function(){u.length&&w.$digest()});u.push({scope:this,expression:a,locals:b})},$$postDigest:function(a){L.push(a)},$apply:function(a){try{t("$apply"); +try{return this.$eval(a)}finally{w.$$phase=null}}catch(b){g(b)}finally{try{w.$digest()}catch(c){throw g(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&aa.push(b);C()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,v(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,f=!1,h= +{name:a,targetScope:e,stopPropagation:function(){f=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=cb([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;lHa)throw ya("iequirks");var c=ia(la);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ya);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;n(la,function(a, +b){var d=F(b);c[fb("parse_as_"+d)]=function(b){return e(a,b)};c[fb("get_trusted_"+d)]=function(b){return f(a,b)};c[fb("trust_as_"+d)]=function(b){return g(a,b)}});return c}]}function qf(){this.$get=["$window","$document",function(a,b){var d={},c=ea((/android (\d+)/.exec(F((a.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((a.navigator||{}).userAgent),f=b[0]||{},g,h=/^(Moz|webkit|ms)(?=[A-Z])/,k=f.body&&f.body.style,l=!1,m=!1;if(k){for(var r in k)if(l=h.exec(r)){g=l[0];g=g.substr(0,1).toUpperCase()+ +g.substr(1);break}g||(g="WebkitOpacity"in k&&"webkit");l=!!("transition"in k||g+"Transition"in k);m=!!("animation"in k||g+"Animation"in k);!c||l&&m||(l=E(k.webkitTransition),m=E(k.webkitAnimation))}return{history:!(!a.history||!a.history.pushState||4>c||e),hasEvent:function(a){if("input"===a&&11>=Ha)return!1;if(q(d[a])){var b=f.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ba(),vendorPrefix:g,transitions:l,animations:m,android:c}}]}function sf(){this.$get=["$templateCache","$http","$q","$sce", +function(a,b,d,c){function e(f,g){e.totalPendingRequests++;E(f)&&a.get(f)||(f=c.getTrustedResourceUrl(f));var h=b.defaults&&b.defaults.transformResponse;I(h)?h=h.filter(function(a){return a!==$b}):h===$b&&(h=null);return b.get(f,{cache:a,transformResponse:h})["finally"](function(){e.totalPendingRequests--}).then(function(b){a.put(f,b.data);return b.data},function(a){if(!g)throw ha("tpload",f,a.status,a.statusText);return d.reject(a)})}e.totalPendingRequests=0;return e}]}function tf(){this.$get=["$rootScope", +"$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];n(a,function(a){var c=fa.element(a).data("$binding");c&&n(c,function(c){d?(new RegExp("(^|\\s)"+ud(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!=c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;ha;a=Math.abs(a);var g=Infinity===a;if(!g&&!isFinite(a))return"";var h=a+"",k="",l=!1,m=[];g&&(k="\u221e");if(!g&&-1!==h.indexOf("e")){var r=h.match(/([\d\.]+)e(-?)(\d+)/);r&&"-"==r[2]&&r[3]>e+1?a=0:(k=h,l=!0)}if(g||l)0a&&(k=a.toFixed(e),a=parseFloat(k),k=k.replace(ic,c));else{g=(h.split(ic)[1]||"").length; +q(e)&&(e=Math.min(Math.max(b.minFrac,g),b.maxFrac));a=+(Math.round(+(a.toString()+"e"+e)).toString()+"e"+-e);var g=(""+a).split(ic),h=g[0],g=g[1]||"",r=0,t=b.lgSize,n=b.gSize;if(h.length>=t+n)for(r=h.length-t,l=0;la&&(c="-",a=-a);for(a=""+a;a.length-d)e+=d;0===e&&-12==d&&(e=12);return Gb(e,b,c)}}function Hb(a,b){return function(d,c){var e=d["get"+a](),f=sb(b?"SHORT"+a:a);return c[f][e]}}function Dd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Ed(a){return function(b){var d=Dd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))- ++d;b=1+Math.round(b/6048E5);return Gb(b,a)}}function jc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function zd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=ea(b[9]+b[10]),g=ea(b[9]+b[11]));h.call(a,ea(b[1]),ea(b[2])-1,ea(b[3]));f=ea(b[4]||0)-f;g=ea(b[5]||0)-g;h=ea(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; +return function(c,d,f){var g="",h=[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;E(c)&&(c=hg.test(c)?ea(c):b(c));Q(c)&&(c=new Date(c));if(!da(c)||!isFinite(c.getTime()))return c;for(;d;)(l=ig.exec(d))?(h=cb(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=vc(f,c.getTimezoneOffset()),c=Pb(c,f,!0));n(h,function(b){k=jg[b];g+=k?k(c,a.DATETIME_FORMATS,m):b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function cg(){return function(a,b){q(b)&&(b=2);return db(a,b)}}function dg(){return function(a, +b,d){b=Infinity===Math.abs(Number(b))?Number(b):ea(b);if(isNaN(b))return a;Q(a)&&(a=a.toString());if(!I(a)&&!E(a))return a;d=!d||isNaN(d)?0:ea(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?a.slice(d,d+b):0===d?a.slice(b,a.length):a.slice(Math.max(0,d+b),d)}}function Bd(a){function b(b,d){d=d?-1:1;return b.map(function(b){var c=1,h=Ya;if(z(b))h=b;else if(E(b)){if("+"==b.charAt(0)||"-"==b.charAt(0))c="-"==b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(h=a(b),h.constant))var k=h(),h=function(a){return a[k]}}return{get:h, +descending:c*d}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}return function(a,e,f){if(!za(a))return a;I(e)||(e=[e]);0===e.length&&(e=["+"]);var g=b(e,f);g.push({get:function(){return{}},descending:f?-1:1});a=Array.prototype.map.call(a,function(a,b){return{value:a,predicateValues:g.map(function(c){var e=c.get(a);c=typeof e;if(null===e)c="string",e="null";else if("string"===c)e=e.toLowerCase();else if("object"===c)a:{if("function"===typeof e.valueOf&& +(e=e.valueOf(),d(e)))break a;if(qc(e)&&(e=e.toString(),d(e)))break a;e=b}return{value:e,type:c}})}});a.sort(function(a,b){for(var c=0,d=0,e=g.length;db||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut", +m)}b.on("change",k);c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Kb(a,b){return function(d,c){var e,f;if(da(d))return d;if(E(d)){'"'==d.charAt(0)&&'"'==d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(kg.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0, +mm:0,ss:0,sss:0},n(e,function(a,c){c=s};g.$observe("min",function(a){s=n(a);h.$validate()})}if(y(g.max)||g.ngMax){var p;h.$validators.max=function(a){return!r(a)||q(p)||d(a)<=p};g.$observe("max",function(a){p=n(a);h.$validate()})}}}function Hd(a,b,d,c){(c.$$hasNativeValidators=H(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{}; +return c.badInput&&!c.typeMismatch?u:a})}function Id(a,b,d,c,e){if(y(c)){a=a(c);if(!a.constant)throw lb("constexpr",d,c);return a(b)}return e}function lc(a,b){a="ngClass"+a;return["$animate",function(d){function c(a,b){var c=[],d=0;a:for(;d(?:<\/\1>|)$/,Tb=/<|&#?\w+;/, +Cf=/<([\w:-]+)/,Df=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ka={option:[1,'"],thead:[1,"
    ","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ka.optgroup=ka.option;ka.tbody=ka.tfoot=ka.colgroup=ka.caption=ka.thead;ka.th=ka.td;var Kf=Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)& +16)},Pa=N.prototype={ready:function(a){function b(){d||(d=!0,a())}var d=!1;"complete"===X.readyState?setTimeout(b):(this.on("DOMContentLoaded",b),N(S).on("load",b))},toString:function(){var a=[];n(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?B(this[a]):B(this[this.length+a])},length:0,push:mg,sort:[].sort,splice:[].splice},Cb={};n("multiple selected checked disabled readOnly required open".split(" "),function(a){Cb[F(a)]=a});var Rc={};n("input select option textarea button form details".split(" "), +function(a){Rc[a]=!0});var Zc={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};n({data:Wb,removeData:vb,hasData:function(a){for(var b in gb[a.ng339])return!0;return!1}},function(a,b){N[b]=a});n({data:Wb,inheritedData:Bb,scope:function(a){return B.data(a,"$scope")||Bb(a.parentNode||a,["$isolateScope","$scope"])},isolateScope:function(a){return B.data(a,"$isolateScope")||B.data(a,"$isolateScopeNoTemplate")},controller:Oc,injector:function(a){return Bb(a, +"$injector")},removeAttr:function(a,b){a.removeAttribute(b)},hasClass:yb,css:function(a,b,d){b=fb(b);if(y(d))a.style[b]=d;else return a.style[b]},attr:function(a,b,d){var c=a.nodeType;if(c!==Na&&2!==c&&8!==c)if(c=F(b),Cb[c])if(y(d))d?(a[b]=!0,a.setAttribute(b,c)):(a[b]=!1,a.removeAttribute(c));else return a[b]||(a.attributes.getNamedItem(b)||x).specified?c:u;else if(y(d))a.setAttribute(b,d);else if(a.getAttribute)return a=a.getAttribute(b,2),null===a?u:a},prop:function(a,b,d){if(y(d))a[b]=d;else return a[b]}, +text:function(){function a(a,d){if(q(d)){var c=a.nodeType;return 1===c||c===Na?a.textContent:""}a.textContent=d}a.$dv="";return a}(),val:function(a,b){if(q(b)){if(a.multiple&&"select"===ta(a)){var d=[];n(a.options,function(a){a.selected&&d.push(a.value||a.text)});return 0===d.length?null:d}return a.value}a.value=b},html:function(a,b){if(q(b))return a.innerHTML;ub(a,!0);a.innerHTML=b},empty:Pc},function(a,b){N.prototype[b]=function(b,c){var e,f,g=this.length;if(a!==Pc&&q(2==a.length&&a!==yb&&a!==Oc? +b:c)){if(H(b)){for(e=0;e <= >= && || ! = |".split(" "),function(a){Lb[a]=!0});var sg={n:"\n",f:"\f",r:"\r", +t:"\t",v:"\v","'":"'",'"':'"'},fc=function(a){this.options=a};fc.prototype={constructor:fc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a|| +"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=y(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw ba("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index","<=",">=");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text, +left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.constants.hasOwnProperty(this.peek().text)?a=bb(this.constants[this.consume().text]):this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant(): +this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(), +arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.expression());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break; +a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:s.Property,kind:"init"};this.peek().constant?b.key=this.constant():this.peek().identifier?b.key=this.identifier():this.throwError("invalid key",this.peek());this.consume(":");b.value=this.expression();a.push(b)}while(this.expect(","))}this.consume("}");return{type:s.ObjectExpression,properties:a}}, +throwError:function(a,b){throw ba("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw ba("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw ba("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a]; +var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},constants:{"true":{type:s.Literal,value:!0},"false":{type:s.Literal,value:!1},"null":{type:s.Literal,value:null},undefined:{type:s.Literal,value:u},"this":{type:s.ThisExpression}}};rd.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.state={nextId:0,filters:{},expensiveChecks:b,fn:{vars:[],body:[],own:{}},assign:{vars:[], +body:[],own:{}},inputs:[]};W(c,d.$filter);var e="",f;this.stage="assign";if(f=pd(c))this.state.computing="assign",e=this.nextId(),this.recurse(f,e),this.return_(e),e="fn.assign="+this.generateFunction("assign","s,v,l");f=nd(c.body);d.stage="inputs";n(f,function(a,b){var c="fn"+b;d.state[c]={vars:[],body:[],own:{}};d.state.computing=c;var e=d.nextId();d.recurse(a,e);d.return_(e);d.state.inputs.push(c);a.watchId=b});this.state.computing="fn";this.stage="main";this.recurse(c);e='"'+this.USE+" "+this.STRICT+ +'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+e+this.watchFns()+"return fn;";e=(new Function("$filter","ensureSafeMemberName","ensureSafeObject","ensureSafeFunction","getStringValue","ensureSafeAssignContext","ifDefined","plus","text",e))(this.$filter,Va,xa,kd,jd,ld,Zf,md,a);this.state=this.stage=u;e.literal=qd(c);e.constant=c.constant;return e},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;n(b,function(b){a.push("var "+b+"="+d.generateFunction(b, +"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;n(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,b, +d,c,e,f){var g,h,k=this,l,m;c=c||x;if(!f&&y(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case s.Program:n(a.body,function(b,c){k.recurse(b.expression,u,u,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(m);break;case s.UnaryExpression:this.recurse(a.argument,u,u,function(a){h=a});m=a.operator+"("+this.ifDefined(h, +0)+")";this.assign(b,m);c(m);break;case s.BinaryExpression:this.recurse(a.left,u,u,function(a){g=a});this.recurse(a.right,u,u,function(a){h=a});m="+"===a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right,b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test, +b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);Va(a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.not(k.nonComputedMember("s",a.name)),k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s", +a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));(k.state.expensiveChecks||Fb(a.name))&&k.addEnsureSafeObject(b);c(b);break;case s.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,u,function(){k.if_(k.notNull(g),function(){if(a.computed)h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),k.addEnsureSafeMemberName(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,h),"{}")),m=k.ensureSafeObject(k.computedMember(g, +h)),k.assign(b,m),d&&(d.computed=!0,d.name=h);else{Va(a.property.name);e&&1!==e&&k.if_(k.not(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}"));m=k.nonComputedMember(g,a.property.name);if(k.state.expensiveChecks||Fb(a.property.name))m=k.ensureSafeObject(m);k.assign(b,m);d&&(d.computed=!1,d.name=a.property.name)}},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();a.filter?(h=k.filter(a.callee.name),l=[],n(a.arguments, +function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+l.join(",")+")",k.assign(b,m),c(b)):(h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){k.addEnsureSafeFunction(h);n(a.arguments,function(a){k.recurse(a,k.nextId(),u,function(a){l.push(k.ensureSafeObject(a))})});g.name?(k.state.expensiveChecks||k.addEnsureSafeObject(g.context),m=k.member(g.context,g.name,g.computed)+"("+l.join(",")+")"):m=h+"("+l.join(",")+")";m=k.ensureSafeObject(m);k.assign(b,m)}, +function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=this.nextId();g={};if(!od(a.left))throw ba("lval");this.recurse(a.left,u,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);k.addEnsureSafeObject(k.member(g.context,g.name,g.computed));k.addEnsureSafeAssignContext(g.context);m=k.member(g.context,g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=[];n(a.elements,function(a){k.recurse(a,k.nextId(),u,function(a){l.push(a)})}); +m="["+l.join(",")+"]";this.assign(b,m);c(m);break;case s.ObjectExpression:l=[];n(a.properties,function(a){k.recurse(a.value,k.nextId(),u,function(b){l.push(k.escape(a.key.type===s.Identifier?a.key.name:""+a.key.value)+":"+b)})});m="{"+l.join(",")+"}";this.assign(b,m);c(m);break;case s.ThisExpression:this.assign(b,"s");c("s");break;case s.NGValueParameter:this.assign(b,"v"),c("v")}},getHasOwnProperty:function(a,b){var d=a+"."+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+ +this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a, +"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){return a+"."+b},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,b)},addEnsureSafeObject:function(a){this.current().body.push(this.ensureSafeObject(a),";")},addEnsureSafeMemberName:function(a){this.current().body.push(this.ensureSafeMemberName(a),";")}, +addEnsureSafeFunction:function(a){this.current().body.push(this.ensureSafeFunction(a),";")},addEnsureSafeAssignContext:function(a){this.current().body.push(this.ensureSafeAssignContext(a),";")},ensureSafeObject:function(a){return"ensureSafeObject("+a+",text)"},ensureSafeMemberName:function(a){return"ensureSafeMemberName("+a+",text)"},ensureSafeFunction:function(a){return"ensureSafeFunction("+a+",text)"},getStringValue:function(a){this.assign(a,"getStringValue("+a+",text)")},ensureSafeAssignContext:function(a){return"ensureSafeAssignContext("+ +a+",text)"},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(E(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(Q(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"=== +typeof a)return"undefined";throw ba("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};sd.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.expression=a;this.expensiveChecks=b;W(c,d.$filter);var e,f;if(e=pd(c))f=this.recurse(e);e=nd(c.body);var g;e&&(g=[],n(e,function(a,b){var c=d.recurse(a);a.input=c;g.push(c);a.watchId=b}));var h=[];n(c.body,function(a){h.push(d.recurse(a.expression))}); +e=0===c.body.length?function(){}:1===c.body.length?h[0]:function(a,b){var c;n(h,function(d){c=d(a,b)});return c};f&&(e.assign=function(a,b,c){return f(a,c,b)});g&&(e.inputs=g);e.literal=qd(c);e.constant=c.constant;return e},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left), +e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return Va(a.name,f.expression),f.identifier(a.name,f.expensiveChecks||Fb(a.name),b,d,f.expression);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(Va(a.property.name, +f.expression),e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d,f.expression):this.nonComputedMember(c,e,f.expensiveChecks,b,d,f.expression);case s.CallExpression:return g=[],n(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var r=[],n=0;n":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c, +e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:u,name:u,value:a}:a}},identifier:function(a,b,d,c,e){return function(f,g,h,k){f= +g&&a in g?g:f;c&&1!==c&&f&&!f[a]&&(f[a]={});g=f?f[a]:u;b&&xa(g,e);return d?{context:f,name:a,value:g}:g}},computedMember:function(a,b,d,c,e){return function(f,g,h,k){var l=a(f,g,h,k),m,n;null!=l&&(m=b(f,g,h,k),m=jd(m),Va(m,e),c&&1!==c&&l&&!l[m]&&(l[m]={}),n=l[m],xa(n,e));return d?{context:l,name:m,value:n}:n}},nonComputedMember:function(a,b,d,c,e,f){return function(g,h,k,l){g=a(g,h,k,l);e&&1!==e&&g&&!g[b]&&(g[b]={});h=null!=g?g[b]:u;(d||Fb(b))&&xa(h,f);return c?{context:g,name:b,value:h}:h}},inputs:function(a, +b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};var gc=function(a,b,d){this.lexer=a;this.$filter=b;this.options=d;this.ast=new s(this.lexer);this.astCompiler=d.csp?new sd(this.ast,b):new rd(this.ast,b)};gc.prototype={constructor:gc,parse:function(a){return this.astCompiler.compile(a,this.options.expensiveChecks)}};$();$();var $f=Object.prototype.valueOf,ya=G("$sce"),la={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},ha=G("$compile"),Y=X.createElement("a"),wd=wa(S.location.href); +xd.$inject=["$document"];Jc.$inject=["$provide"];yd.$inject=["$locale"];Ad.$inject=["$locale"];var ic=".",jg={yyyy:ca("FullYear",4),yy:ca("FullYear",2,0,!0),y:ca("FullYear",1),MMMM:Hb("Month"),MMM:Hb("Month",!0),MM:ca("Month",2,1),M:ca("Month",1,1),dd:ca("Date",2),d:ca("Date",1),HH:ca("Hours",2),H:ca("Hours",1),hh:ca("Hours",2,-12),h:ca("Hours",1,-12),mm:ca("Minutes",2),m:ca("Minutes",1),ss:ca("Seconds",2),s:ca("Seconds",1),sss:ca("Milliseconds",3),EEEE:Hb("Day"),EEE:Hb("Day",!0),a:function(a,b){return 12> +a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Gb(Math[0=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},ig=/((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,hg=/^\-?\d+$/;zd.$inject=["$locale"];var eg=na(F),fg=na(sb);Bd.$inject=["$parse"];var he=na({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a, +b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===sa.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),tb={};n(Cb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!=a){var c=va("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});tb[c]=function(){return{restrict:"A",priority:100,link:e}}}});n(Zc,function(a,b){tb[b]=function(){return{priority:100,link:function(a, +c,e){if("ngPattern"===b&&"/"==e.ngPattern.charAt(0)&&(c=e.ngPattern.match(lg))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});n(["src","srcset","href"],function(a){var b=va("ng-"+a);tb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===sa.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b),Ha&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}}); +var Ib={$addControl:x,$$renameControl:function(a,b){a.$name=b},$removeControl:x,$setValidity:x,$setDirty:x,$setPristine:x,$setSubmitted:x};Fd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var Nd=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||x}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Fd,compile:function(d,f){d.addClass(Wa).addClass(mb);var g=f.name?"name":a&&f.ngForm?"ngForm": +!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in e)){var q=function(b){a.$apply(function(){n.$commitViewValue();n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",q,!1);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",q,!1)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n);var s=g?c(n.$name):x;g&&(s(a,n),e.$observe(g,function(b){n.$name!==b&&(s(a,u),n.$$parentForm.$$renameControl(n,b),s=c(n.$name),s(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n); +s(a,u);M(n,Ib)})}}}}}]},ie=Nd(),ve=Nd(!0),kg=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/,tg=/^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/,ug=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,vg=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,Od=/^(\d{4})-(\d{2})-(\d{2})$/,Pd=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,mc=/^(\d{4})-W(\d\d)$/,Qd=/^(\d{4})-(\d\d)$/, +Rd=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Sd={text:function(a,b,d,c,e,f){jb(a,b,d,c,e,f);kc(c)},date:kb("date",Od,Kb(Od,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":kb("datetimelocal",Pd,Kb(Pd,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:kb("time",Rd,Kb(Rd,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:kb("week",mc,function(a,b){if(da(a))return a;if(E(a)){mc.lastIndex=0;var d=mc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Dd(c),e=7*(e-1);b&&(d=b.getHours(),f= +b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:kb("month",Qd,Kb(Qd,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Hd(a,b,d,c);jb(a,b,d,c,e,f);c.$$parserName="number";c.$parsers.push(function(a){return c.$isEmpty(a)?null:vg.test(a)?parseFloat(a):u});c.$formatters.push(function(a){if(!c.$isEmpty(a)){if(!Q(a))throw lb("numfmt",a);a=a.toString()}return a});if(y(d.min)||d.ngMin){var g;c.$validators.min=function(a){return c.$isEmpty(a)|| +q(g)||a>=g};d.$observe("min",function(a){y(a)&&!Q(a)&&(a=parseFloat(a,10));g=Q(a)&&!isNaN(a)?a:u;c.$validate()})}if(y(d.max)||d.ngMax){var h;c.$validators.max=function(a){return c.$isEmpty(a)||q(h)||a<=h};d.$observe("max",function(a){y(a)&&!Q(a)&&(a=parseFloat(a,10));h=Q(a)&&!isNaN(a)?a:u;c.$validate()})}},url:function(a,b,d,c,e,f){jb(a,b,d,c,e,f);kc(c);c.$$parserName="url";c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||tg.test(d)}},email:function(a,b,d,c,e,f){jb(a,b,d,c,e,f);kc(c); +c.$$parserName="email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||ug.test(d)}},radio:function(a,b,d,c){q(d.name)&&b.attr("name",++nb);b.on("click",function(a){b[0].checked&&c.$setViewValue(d.value,a&&a.type)});c.$render=function(){b[0].checked=d.value==c.$viewValue};d.$observe("value",c.$render)},checkbox:function(a,b,d,c,e,f,g,h){var k=Id(h,a,"ngTrueValue",d.ngTrueValue,!0),l=Id(h,a,"ngFalseValue",d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&& +a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return ma(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:x,button:x,submit:x,reset:x,file:x},Dc=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(Sd[F(g.type)]||Sd.text)(e,f,g,h[0],b,a,d,c)}}}}],wg=/^(true|false|\d+)$/,Ne=function(){return{restrict:"A",priority:100,compile:function(a, +b){return wg.test(b.ngValue)?function(a,b,e){e.$set("value",a.$eval(e.ngValue))}:function(a,b,e){a.$watch(e.ngValue,function(a){e.$set("value",a)})}}}},ne=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=q(a)?"":a})}}}}],pe=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate)); +b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=q(a)?"":a})}}}}],oe=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){c.html(a.getTrustedHtml(f(b))||"")})}}}}],Me=na({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}), +qe=lc("",!0),se=lc("Odd",0),re=lc("Even",1),te=La({compile:function(a,b){b.$set("ngCloak",u);a.removeClass("ng-cloak")}}),ue=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Ic={},xg={blur:!0,focus:!0};n("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=va("ng-"+a);Ic[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g= +d(f[b],null,!0);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};xg[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var xe=["$animate",function(a){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(b,d,c,e,f){var g,h,k;b.$watch(c.ngIf,function(b){b?h||f(function(b,e){h=e;b[b.length++]=X.createComment(" end ngIf: "+c.ngIf+" ");g={clone:b};a.enter(b,d.parent(),d)}):(k&&(k.remove(),k=null),h&&(h.$destroy(),h=null),g&&(k= +rb(g.clone),a.leave(k).then(function(){k=null}),g=null))})}}}],ye=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:fa.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,q){var s=0,v,u,p,C=function(){u&&(u.remove(),u=null);v&&(v.$destroy(),v=null);p&&(d.leave(p).then(function(){u=null}),u=p,p=null)};c.$watch(f,function(f){var m=function(){!y(h)||h&&!c.$eval(h)|| +b()},u=++s;f?(a(f,!0).then(function(a){if(u===s){var b=c.$new();n.template=a;a=q(b,function(a){C();d.enter(a,null,e).then(m)});v=b;p=a;v.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){u===s&&(C(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(C(),n.template=null)})}}}}],Pe=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){/SVG/.test(d[0].toString())?(d.empty(),a(Lc(e.template,X).childNodes)(b,function(a){d.append(a)}, +{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],ze=La({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),Le=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=b.attr(d.$attr.ngList)||", ",f="false"!==d.ngTrim,g=f?U(e):e;c.$parsers.push(function(a){if(!q(a)){var b=[];a&&n(a.split(g),function(a){a&&b.push(f?U(a):a)});return b}});c.$formatters.push(function(a){return I(a)?a.join(e):u});c.$isEmpty=function(a){return!a|| +!a.length}}}},mb="ng-valid",Jd="ng-invalid",Wa="ng-pristine",Jb="ng-dirty",Ld="ng-pending",lb=G("ngModel"),yg=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,b,d,c,e,f,g,h,k,l){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=u;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1; +this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=u;this.$name=l(d.name||"",!1)(a);this.$$parentForm=Ib;var m=e(d.ngModel),r=m.assign,t=m,s=r,v=null,B,p=this;this.$$setOptions=function(a){if((p.$options=a)&&a.getterSetter){var b=e(d.ngModel+"()"),f=e(d.ngModel+"($$$p)");t=function(a){var c=m(a);z(c)&&(c=b(a));return c};s=function(a,b){z(m(a))?f(a,{$$$p:p.$modelValue}):r(a,p.$modelValue)}}else if(!m.assign)throw lb("nonassign",d.ngModel,ua(c));};this.$render=x;this.$isEmpty= +function(a){return q(a)||""===a||null===a||a!==a};var C=0;Gd({ctrl:this,$element:c,set:function(a,b){a[b]=!0},unset:function(a,b){delete a[b]},$animate:f});this.$setPristine=function(){p.$dirty=!1;p.$pristine=!0;f.removeClass(c,Jb);f.addClass(c,Wa)};this.$setDirty=function(){p.$dirty=!0;p.$pristine=!1;f.removeClass(c,Wa);f.addClass(c,Jb);p.$$parentForm.$setDirty()};this.$setUntouched=function(){p.$touched=!1;p.$untouched=!0;f.setClass(c,"ng-untouched","ng-touched")};this.$setTouched=function(){p.$touched= +!0;p.$untouched=!1;f.setClass(c,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){g.cancel(v);p.$viewValue=p.$$lastCommittedViewValue;p.$render()};this.$validate=function(){if(!Q(p.$modelValue)||!isNaN(p.$modelValue)){var a=p.$$rawModelValue,b=p.$valid,c=p.$modelValue,d=p.$options&&p.$options.allowInvalid;p.$$runValidators(a,p.$$lastCommittedViewValue,function(e){d||b===e||(p.$modelValue=e?a:u,p.$modelValue!==c&&p.$$writeModelToScope())})}};this.$$runValidators=function(a,b,c){function d(){var c= +!0;n(p.$validators,function(d,e){var g=d(a,b);c=c&&g;f(e,g)});return c?!0:(n(p.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;n(p.$asyncValidators,function(e,g){var h=e(a,b);if(!h||!z(h.then))throw lb("$asyncValidators",h);f(g,u);c.push(h.then(function(){f(g,!0)},function(a){d=!1;f(g,!1)}))});c.length?k.all(c).then(function(){g(d)},x):g(!0)}function f(a,b){h===C&&p.$setValidity(a,b)}function g(a){h===C&&c(a)}C++;var h=C;(function(){var a=p.$$parserName||"parse";if(q(B))f(a, +null);else return B||(n(p.$validators,function(a,b){f(b,null)}),n(p.$asyncValidators,function(a,b){f(b,null)})),f(a,B),B;return!0})()?d()?e():g(!1):g(!1)};this.$commitViewValue=function(){var a=p.$viewValue;g.cancel(v);if(p.$$lastCommittedViewValue!==a||""===a&&p.$$hasNativeValidators)p.$$lastCommittedViewValue=a,p.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var b=p.$$lastCommittedViewValue;if(B=q(b)?u:!0)for(var c=0;ce||c.$isEmpty(b)||b.length<=e}}}}},Gc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=ea(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};S.angular.bootstrap? +console.log("WARNING: Tried to load angular more than once."):(ce(),ee(fa),fa.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "), +SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4", +negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",pluralCat:function(a,c){var e=a|0,f=c;u===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),B(X).ready(function(){Zd(X,yc)}))})(window,document);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(''); +//# sourceMappingURL=angular.min.js.map diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/bootstrap.min.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/bootstrap.min.js new file mode 100644 index 0000000..1a6258e --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.0.3 (http://getbootstrap.com) + * Copyright 2013 Twitter, Inc. + * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + */ + +if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/g2.min.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/g2.min.js new file mode 100644 index 0000000..74ec954 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/g2.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.G2_3=e():t.G2_3=e()}("undefined"!=typeof self?self:this,function(){return function(t){function e(i){if(n[i])return n[i].exports;var r=n[i]={i:i,l:!1,exports:{}};return t[i].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.d=function(t,n,i){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:i})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=389)}([function(t,e,n){var i=n(127),r=n(16),a=i.mix({},i,{assign:i.mix,merge:i.deepMix,cloneDeep:i.clone,isFinite:isFinite,isNaN:isNaN,snapEqual:i.isNumberEqual,remove:i.pull,inArray:i.contains,toAllPadding:function(t){var e=0,n=0,i=0,r=0;return a.isNumber(t)||a.isString(t)?e=n=i=r=t:a.isArray(t)?(e=t[0],i=a.isNil(t[1])?t[0]:t[1],r=a.isNil(t[2])?t[0]:t[2],n=a.isNil(t[3])?i:t[3]):a.isObject(t)&&(e=t.top||0,i=t.right||0,r=t.bottom||0,n=t.left||0),[e,i,r,n]},getClipByRange:function(t){var e=t.tl,n=t.br;return new r.Rect({attrs:{x:e.x,y:e.y,width:n.x-e.x,height:n.y-e.y}})}});a.Array={groupToMap:i.groupToMap,group:i.group,merge:i.merge,values:i.valuesOfKey,getRange:i.getRange,firstValue:i.firstValue,remove:i.pull},t.exports=a},function(t,e,n){var i=n(81),r={};i.merge(r,i,{mixin:function(t,e){var n=t.CFG?"CFG":"ATTRS";if(t&&e){t._mixins=e,t[n]=t[n]||{};var i={};r.each(e,function(e){r.augment(t,e);var a=e[n];a&&r.merge(i,a)}),t[n]=r.merge(i,t[n])}}}),t.exports=r},function(t,e,n){var i=n(24),r=n(4);t.exports=function(t,e){if(t)if(r(t))for(var n=0,a=t.length;n0){var a=e.strokeOpacity;i.isNil(a)||1===a||(t.globalAlpha=a),t.stroke()}}this.afterPath(t)},afterPath:function(){},isHitBox:function(){return!0},isHit:function(t,e){var n=[t,e,1];if(this.invert(n),this.isHitBox()){var i=this.getBBox();if(i&&!o.box(i.minX,i.maxX,i.minY,i.maxY,n[0],n[1]))return!1}var r=this._attrs.clip;return r?(r.invert(n,this.get("canvas")),!!r.isPointInPath(n[0],n[1])&&this.isPointInPath(n[0],n[1])):this.isPointInPath(n[0],n[1])},calculateBox:function(){return null},getHitLineWidth:function(){var t=this._attrs,e=t.lineAppendWidth||0;return(t.lineWidth||0)+e},clearTotalMatrix:function(){this._cfg.totalMatrix=null,this._cfg.region=null},clearBBox:function(){this._cfg.box=null,this._cfg.region=null},getBBox:function(){var t=this._cfg.box;return t||((t=this.calculateBox())&&(t.x=t.minX,t.y=t.minY,t.width=t.maxX-t.minX,t.height=t.maxY-t.minY),this._cfg.box=t),t},clone:function(){var t=null,e=this._attrs,n={};return i.each(e,function(t,r){l[r]&&i.isArray(e[r])?n[r]=function(t){for(var e=[],n=0;n1){var y=f[1];y.change({nice:!1,min:0,max:Math.max.apply(null,y.values)})}s.scales=f;var x=new a[c](s);t[o]=x}},n._processData=function(){for(var t=this.get("data"),e=[],n=this._groupData(t),i=0;ia&&(a=c)}(re.max)&&e.change({min:r,max:a})},n._adjust=function(t){var e=this,n=e.get("adjusts"),i=this.viewTheme||u,r=e.getYScale(),a=e.getXScale(),s=a.field,c=r?r.field:null;l.each(n,function(n){var u=l.mix({xField:s,yField:c},n),h=l.upperFirst(n.type);if("Dodge"===h){var f=[];if(a.isCategory||a.isIdentity)f.push("x");else{if(r)throw new Error("dodge is not support linear attribute, please use category attribute!");f.push("y")}u.adjustNames=f,u.dodgeRatio=i.widthRatio.column}else if("Stack"===h){var p=e.get("coord");if(!r){u.height=p.getHeight();var g=e.getDefaultValue("size")||3;u.size=g}!p.isTransposed&&l.isNil(u.reverseOrder)&&(u.reverseOrder=!0)}new o[h](u).processAdjust(t),"Stack"===h&&r&&e._updateStackRange(c,r,t)})},n.setCoord=function(t){this.set("coord",t);var e=this.getAttr("position");this.get("shapeContainer").setMatrix(t.matrix),e&&(e.coord=t)},n.paint=function(){var t=this.get("dataArray"),e=[],n=this.getShapeFactory();n.setCoord(this.get("coord")),this.set("shapeFactory",n);var i=this.get("shapeContainer");this._beforeMapping(t);for(var r=0;r=0?e:n<=0?n:0},n._normalizeValues=function(t,e){var n=[];if(l.isArray(t))for(var i=0;i1)for(var h=0;h0)l.each(n,function(n){e+="-"+t[n]});else{var i,r=this.get("type"),a=this.getXScale(),o=this.getYScale(),s=a.field||"x",u=o.field||"y",c=t[u];i=a.isIdentity?a.value:t[s],e+="interval"===r||"schema"===r?"-"+i:"line"===r||"area"===r||"path"===r?"-"+r:"-"+i+"-"+c;var h=this._getGroupScales();l.isEmpty(h)||l.each(h,function(n){var i=n.field;"identity"!==n.type&&(e+="-"+t[i])})}return e},n.getDrawCfg=function(t){var e={origin:t,x:t.x,y:t.y,color:t.color,size:t.size,shape:t.shape,isInCircle:this.isInCircle(),opacity:t.opacity},n=this.get("styleOptions");return n&&n.style&&(e.style=this.getCallbackCfg(n.fields,n.style,t._origin)),this.get("generatePoints")&&(e.points=t.points,e.nextPoints=t.nextPoints),this.get("animate")&&(e._id=this._getShapeId(t._origin)),e},n.appendShapeInfo=function(t,e){t&&(t.setSilent("index",e),t.setSilent("coord",this.get("coord")),this.get("animate")&&this.get("animateCfg")&&t.setSilent("animateCfg",this.get("animateCfg")))},n._applyViewThemeShapeStyle=function(t,e,n){var i=this.viewTheme||u,r=n.name;e?e&&(e.indexOf("hollow")>-1||e.indexOf("liquid")>-1)&&(r="hollow"+l.upperFirst(r)):n.defaultShapeType.indexOf("hollow")>-1&&(r="hollow"+l.upperFirst(r));var a=i.shape[r]||{};t.style=l.mix({},a,t.style)},n.drawPoint=function(t,e,n,i){var r=t.shape,a=this.getDrawCfg(t);this._applyViewThemeShapeStyle(a,r,n);var o=n.drawShape(r,a,e);this.appendShapeInfo(o,i)},n.getAttr=function(t){return this.get("attrs")[t]},n.getXScale=function(){return this.getAttr("position").scales[0]},n.getYScale=function(){return this.getAttr("position").scales[1]},n.getShapes=function(){var t=[],e=this.get("shapeContainer").get("children");return l.each(e,function(e){e.get("origin")&&t.push(e)}),t},n.getAttrsForLegend=function(){var t=this.get("attrs"),e=[];return l.each(t,function(t){-1!==v.indexOf(t.type)&&e.push(t)}),e},n.getFieldsForLegend=function(){var t=[],e=this.get("attrOptions");return l.each(v,function(n){var i=e[n];i&&i.field&&l.isString(i.field)&&(t=t.concat(i.field.split("*")))}),l.uniq(t)},n.changeVisible=function(t,e){this.set("visible",t);var n=this.get("shapeContainer");n&&n.set("visible",t);var i=this.get("labelContainer");if(i&&i.set("visible",t),!e&&n){n.get("canvas").draw()}},n.reset=function(){this.set("attrOptions",{}),this.clearInner()},n.clearInner=function(){this.clearActivedShapes(),this.clearSelected();var t=this.get("shapeContainer");t&&t.clear();var e=this.get("labelContainer");e&&e.remove(),this.set("attrs",{}),this.set("groupScales",null),this.set("labelContainer",null),this.set("xDistance",null),this.set("isStacked",null)},n.clear=function(){this.clearInner(),this.set("scales",{})},n.destroy=function(){this.clear();var e=this.get("shapeContainer");e&&e.remove(),this.offEvents(),t.prototype.destroy.call(this)},n.bindEvents=function(){this.get("view")&&(this._bindActiveAction(),this._bindSelectedAction())},n.offEvents=function(){this.get("view")&&(this._offActiveAction(),this._offSelectedAction())},e}(s);t.exports=y},function(t,e,n){t.exports={Axis:n(306),Component:n(66),Guide:n(314),Label:n(323),Legend:n(324),Tooltip:n(330)}},function(t,e,n){function i(t,e){var n=t.getCenter();return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function r(t,e){for(var n=t.length,i=[t[0]],r=1;r=s[c]?1:0,p=h>Math.PI?1:0,g=n.convertPoint(l),d=i(n,g);if(d>=.5)if(h===2*Math.PI){var v={x:(l.x+s.x)/2,y:(l.y+s.y)/2},y=n.convertPoint(v);u.push(["A",d,d,0,p,f,y.x,y.y]),u.push(["A",d,d,0,p,f,g.x,g.y])}else u.push(["A",d,d,0,p,f,g.x,g.y]);return u}(n,o,t)):u.push(r(a,t));break;case"z":default:u.push(a)}}),function(t){a.each(t,function(e,n){if("a"===e[0].toLowerCase()){var i=t[n-1],r=t[n+1];r&&"a"===r[0].toLowerCase()?i&&"l"===i[0].toLowerCase()&&(i[0]="M"):i&&"a"===i[0].toLowerCase()&&r&&"l"===r[0].toLowerCase()&&(r[0]="M")}})}(u),u}};t.exports=s},function(t,e,n){var i=n(5);t.exports=function(t){return i(t)?"":t.toString()}},function(t,e){var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};t.exports=function(t){var e=void 0===t?"undefined":n(t);return null!==t&&"object"===e||"function"===e}},function(t,e,n){t.exports={Canvas:n(181),Group:n(101),Shape:n(6),Arc:n(105),Circle:n(106),Dom:n(107),Ellipse:n(108),Fan:n(109),Image:n(110),Line:n(111),Marker:n(56),Path:n(112),Polygon:n(113),Polyline:n(114),Rect:n(115),Text:n(116),PathSegment:n(39),PathUtil:n(57),Event:n(100),version:"3.3.5"}},function(t,e,n){var i=n(48),r=n(12);t.exports=function(t){if(!i(t)||!r(t,"Object"))return!1;if(null===Object.getPrototypeOf(t))return!0;for(var e=t;null!==Object.getPrototypeOf(e);)e=Object.getPrototypeOf(e);return Object.getPrototypeOf(t)===e}},function(t,e,n){var i=n(1),r=/[MLHVQTCSAZ]([^MLHVQTCSAZ]*)/gi,a=/[^\s\,]+/gi;t.exports={parseRadius:function(t){var e=0,n=0,r=0,a=0;return i.isArray(t)?1===t.length?e=n=r=a=t[0]:2===t.length?(e=r=t[0],n=a=t[1]):3===t.length?(e=t[0],n=a=t[1],r=t[2]):(e=t[0],n=t[1],r=t[2],a=t[3]):e=n=r=a=t,{r1:e,r2:n,r3:r,r4:a}},parsePath:function(t){return t=t||[],i.isArray(t)?t:i.isString(t)?(t=t.match(r),i.each(t,function(e,n){if((e=e.match(a))[0].length>1){var r=e[0].charAt(0);e.splice(1,0,e[0].substr(1)),e[0]=r}i.each(e,function(t,n){isNaN(t)||(e[n]=+t)}),t[n]=e}),t):void 0}}},function(t,e,n){"use strict";function i(t,e){return function(n){return t+n*e}}function r(t,e){var n=e-t;return n?i(t,n):Object(a.a)(isNaN(t)?e:t)}e.c=function(t,e){var n=e-t;return n?i(t,n>180||n<-180?n-360*Math.round(n/360):n):Object(a.a)(isNaN(t)?e:t)},e.b=function(t){return 1==(t=+t)?r:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(i){return Math.pow(t+i*e,n)}}(e,n,t):Object(a.a)(isNaN(e)?n:e)}},e.a=r;var a=n(121)},function(t,e,n){function i(t,e){return r(e)?e:t.invert(t.scale(e))}var r=n(10),a=n(4),o=n(5),s=n(8),l=n(2),u=function(){function t(t){var e=this;this.type="base",this.name=null,this.method=null,this.values=[],this.scales=[],this.linear=null;var n=null,i=this.callback;if(t.callback){var r=t.callback;n=function(){for(var t=arguments.length,n=new Array(t),a=0;a1&&(e=(t[1].value-t[0].value)/2);for(var n=[],i=0;i0){var s=e.value-a[r-1].value;s/=t.get("subTickCount")+1;for(var l=1;l<=n;l++){var u={text:"",value:r?a[r-1].value+l*s:l*s},c=t.getTickPoint(u.value),h=void 0;h=o&&o.length?o.length:parseInt(.6*i.length,10),t._addTickItem(l-1,c,h,"sub")}}})}},n._addTickLine=function(t,e){var n=r.mix({},e),i=[];r.each(t,function(t){i.push(["M",t.x1,t.y1]),i.push(["L",t.x2,t.y2])}),delete n.length,n.path=i;var a=this.get("group").addShape("path",{attrs:n});a.name="axis-ticks",a._id=this.get("_id")+"-ticks",a.set("coord",this.get("coord")),this.get("appendInfo")&&a.setSilent("appendInfo",this.get("appendInfo"))},n._renderTicks=function(){var t=this.get("tickItems"),e=this.get("subTickItems");if(!r.isEmpty(t)){var n=this.get("tickLine");this._addTickLine(t,n)}if(!r.isEmpty(e)){var i=this.get("subTickLine")||this.get("tickLine");this._addTickLine(e,i)}},n._renderGrid=function(){var t=this.get("grid");if(t){t.coord=this.get("coord"),t.appendInfo=this.get("appendInfo");var e=this.get("group");this.set("gridGroup",e.addGroup(a,t))}},n._renderLabels=function(){var t=this.get("labelRenderer"),e=this.get("labelItems");t&&(t.set("items",e),t._dryDraw())},n.paint=function(){var t=this.get("tickLine"),e=!0;t&&t.hasOwnProperty("alignWithLabel")&&(e=t.alignWithLabel),this._renderLine();var n=this.get("type");("cat"===n||"timeCat"===n)&&!1===e?this._processCatTicks():this._processTicks(),this._renderTicks(),this._renderGrid(),this._renderLabels();var i=this.get("label");i&&i.autoRotate&&this.autoRotateLabels(),i&&i.autoHide&&this.autoHideLabels()},n.parseTick=function(t,e,n){return{text:t,value:e/(n-1)}},n.getTextAnchor=function(t){return Math.abs(t[1]/t[0])>=1?"center":t[0]>0?"start":"end"},n.getMaxLabelWidth=function(t){var e=t.getLabels(),n=0;return r.each(e,function(t){var e=t.getBBox().width;ne)&&(this.min=e),(i(this.max)||this.max=t.min&&e<=t.max&&n.push(e)}),n.length||(n.push(t.min),n.push(t.max)),t.ticks=n}},n.scale=function(t){if(i(t))return NaN;var e=this.max,n=this.min;if(e===n)return 0;var r=(t-n)/(e-n),a=this.rangeMin();return a+r*(this.rangeMax()-a)},n.invert=function(t){var e=(t-this.rangeMin())/(this.rangeMax()-this.rangeMin());return this.min+e*(this.max-this.min)},e}(a);a.Linear=s,t.exports=s},function(t,e,n){var i=n(13);t.exports=function(t){return i(t)?Array.prototype.slice.call(t):[]}},function(t,e){t.exports=function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1e-5;return Math.abs(t-e)n&&(r=2*Math.PI-t+e,a=t-n):(r=t-e,a=n-t),r>a?n:e}function a(t,e,n,i){var a=0;return n-e>=2*Math.PI&&(a=2*Math.PI),e=s.mod(e,2*Math.PI),n=s.mod(n,2*Math.PI)+a,t=s.mod(t,2*Math.PI),i?e>=n?t>n&&tn?t:r(t,e,n):e<=n?ee||tt.x&&(g=t.x),dt.y&&(v=t.y),y0&&p>0?h=Math.PI/2-g:f<0&&p<0?h=-Math.PI/2-g:f>=0&&p<0?h=-g-Math.PI/2:f<=0&&p>0&&(h=Math.PI/2-g);var d=function(t){var e,n=[],i=a.parsePath(t.path);if(!Array.isArray(i)||0===i.length||"M"!==i[0][0]&&"m"!==i[0][0])return!1;for(var r=i.length,s=0;s=0,p=f?n.toUpperCase():n,g=t,v=e.endPoint,y=g[1],x=g[2];switch(p){default:break;case"M":h=f?i(y,x,v):{x:y,y:x},this.command="M",this.params=[v,h],this.subStart=h,this.endPoint=h;break;case"L":h=f?i(y,x,v):{x:y,y:x},this.command="L",this.params=[v,h],this.subStart=e.subStart,this.endPoint=h,this.endTangent=function(){return[h.x-v.x,h.y-v.y]},this.startTangent=function(){return[v.x-h.x,v.y-h.y]};break;case"H":h=f?i(y,0,v):{x:y,y:v.y},this.command="L",this.params=[v,h],this.subStart=e.subStart,this.endPoint=h,this.endTangent=function(){return[h.x-v.x,h.y-v.y]},this.startTangent=function(){return[v.x-h.x,v.y-h.y]};break;case"V":h=f?i(0,y,v):{x:v.x,y:y},this.command="L",this.params=[v,h],this.subStart=e.subStart,this.endPoint=h,this.endTangent=function(){return[h.x-v.x,h.y-v.y]},this.startTangent=function(){return[v.x-h.x,v.y-h.y]};break;case"Q":f?(a=i(y,x,v),u=i(g[3],g[4],v)):(a={x:y,y:x},u={x:g[3],y:g[4]}),this.command="Q",this.params=[v,a,u],this.subStart=e.subStart,this.endPoint=u,this.endTangent=function(){return[u.x-a.x,u.y-a.y]},this.startTangent=function(){return[v.x-a.x,v.y-a.y]};break;case"T":u=f?i(y,x,v):{x:y,y:x},"Q"===e.command?(a=r(e.params[1],v),this.command="Q",this.params=[v,a,u],this.subStart=e.subStart,this.endPoint=u,this.endTangent=function(){return[u.x-a.x,u.y-a.y]},this.startTangent=function(){return[v.x-a.x,v.y-a.y]}):(this.command="TL",this.params=[v,u],this.subStart=e.subStart,this.endPoint=u,this.endTangent=function(){return[u.x-v.x,u.y-v.y]},this.startTangent=function(){return[v.x-u.x,v.y-u.y]});break;case"C":f?(a=i(y,x,v),u=i(g[3],g[4],v),c=i(g[5],g[6],v)):(a={x:y,y:x},u={x:g[3],y:g[4]},c={x:g[5],y:g[6]}),this.command="C",this.params=[v,a,u,c],this.subStart=e.subStart,this.endPoint=c,this.endTangent=function(){return[c.x-u.x,c.y-u.y]},this.startTangent=function(){return[v.x-a.x,v.y-a.y]};break;case"S":f?(u=i(y,x,v),c=i(g[3],g[4],v)):(u={x:y,y:x},c={x:g[3],y:g[4]}),"C"===e.command?(a=r(e.params[2],v),this.command="C",this.params=[v,a,u,c],this.subStart=e.subStart,this.endPoint=c,this.endTangent=function(){return[c.x-u.x,c.y-u.y]},this.startTangent=function(){return[v.x-a.x,v.y-a.y]}):(this.command="SQ",this.params=[v,u,c],this.subStart=e.subStart,this.endPoint=c,this.endTangent=function(){return[c.x-u.x,c.y-u.y]},this.startTangent=function(){return[v.x-u.x,v.y-u.y]});break;case"A":var m=y,_=x,b=g[3],w=g[4],S=g[5];h=f?i(g[6],g[7],v):{x:g[6],y:g[7]},this.command="A";var M=function(t,e,n,i,r,a,u){var c=l.mod(l.toRadian(u),2*Math.PI),h=t.x,f=t.y,p=e.x,g=e.y,d=Math.cos(c)*(h-p)/2+Math.sin(c)*(f-g)/2,v=-1*Math.sin(c)*(h-p)/2+Math.cos(c)*(f-g)/2,y=d*d/(r*r)+v*v/(a*a);y>1&&(r*=Math.sqrt(y),a*=Math.sqrt(y));var x=r*r*(v*v)+a*a*(d*d),m=Math.sqrt((r*r*(a*a)-x)/x);n===i&&(m*=-1),isNaN(m)&&(m=0);var _=m*r*v/a,b=m*-a*d/r,w=(h+p)/2+Math.cos(c)*_-Math.sin(c)*b,S=(f+g)/2+Math.sin(c)*_+Math.cos(c)*b,M=s([1,0],[(d-_)/r,(v-b)/a]),C=[(d-_)/r,(v-b)/a],A=[(-1*d-_)/r,(-1*v-b)/a],k=s(C,A);return o(C,A)<=-1&&(k=Math.PI),o(C,A)>=1&&(k=0),0===i&&k>0&&(k-=2*Math.PI),1===i&&k<0&&(k+=2*Math.PI),[t,w,S,r,a,M,k,c,i]}(v,h,w,S,m,_,b);this.params=M;var C=e.subStart;this.subStart=C,this.endPoint=h;var A=M[5]%(2*Math.PI);l.isNumberEqual(A,2*Math.PI)&&(A=0);var k=M[6]%(2*Math.PI);l.isNumberEqual(k,2*Math.PI)&&(k=0);var P=.001;this.startTangent=function(){0===S&&(P*=-1);var t=M[3]*Math.cos(A-P)+M[1],e=M[4]*Math.sin(A-P)+M[2];return[t-C.x,e-C.y]},this.endTangent=function(){var t=M[6];t-2*Math.PI<1e-4&&(t=0);var e=M[3]*Math.cos(A+t+P)+M[1],n=M[4]*Math.sin(A+t-P)+M[2];return[v.x-e,v.y-n]};break;case"Z":this.command="Z",this.params=[v,e.subStart],this.subStart=e.subStart,this.endPoint=e.subStart}},isInside:function(t,e,n){var i=this.command,r=this.params,a=this.box;if(a&&!u.box(a.minX,a.maxX,a.minY,a.maxY,t,e))return!1;switch(i){default:break;case"M":return!1;case"TL":case"L":case"Z":return u.line(r[0].x,r[0].y,r[1].x,r[1].y,n,t,e);case"SQ":case"Q":return u.quadraticline(r[0].x,r[0].y,r[1].x,r[1].y,r[2].x,r[2].y,n,t,e);case"C":return u.cubicline(r[0].x,r[0].y,r[1].x,r[1].y,r[2].x,r[2].y,r[3].x,r[3].y,n,t,e);case"A":var o=r,s=o[1],l=o[2],c=o[3],h=o[4],f=o[5],d=o[6],v=o[7],y=o[8],x=c>h?c:h,m=c>h?1:c/h,_=c>h?h/c:1;o=[t,e,1];var b=[1,0,0,0,1,0,0,0,1];return g.translate(b,b,[-s,-l]),g.rotate(b,b,-v),g.scale(b,b,[1/m,1/_]),p.transformMat3(o,o,b),u.arcline(0,0,x,f,f+d,1-y,n,o[0],o[1])}return!1},draw:function(t){var e,n,i,r=this.command,a=this.params;switch(r){default:break;case"M":t.moveTo(a[1].x,a[1].y);break;case"TL":case"L":t.lineTo(a[1].x,a[1].y);break;case"SQ":case"Q":e=a[1],n=a[2],t.quadraticCurveTo(e.x,e.y,n.x,n.y);break;case"C":e=a[1],n=a[2],i=a[3],t.bezierCurveTo(e.x,e.y,n.x,n.y,i.x,i.y);break;case"A":var o=a,s=o[1],l=o[2],u=o[3],c=o[4],h=o[5],f=o[6],p=o[7],g=o[8],d=u>c?u:c,v=u>c?1:u/c,y=u>c?c/u:1;t.translate(s,l),t.rotate(p),t.scale(v,y),t.arc(0,0,d,h,h+f,1-g),t.scale(1/v,1/y),t.rotate(-p),t.translate(-s,-l);break;case"Z":t.closePath()}},getBBox:function(t){var e,n,i,r,a=t/2,o=this.params;switch(this.command){default:case"M":case"Z":break;case"TL":case"L":this.box={minX:Math.min(o[0].x,o[1].x)-a,maxX:Math.max(o[0].x,o[1].x)+a,minY:Math.min(o[0].y,o[1].y)-a,maxY:Math.max(o[0].y,o[1].y)+a};break;case"SQ":case"Q":for(i=0,r=(n=h.extrema(o[0].x,o[1].x,o[2].x)).length;iS&&(S=A)}var k=f.yExtrema(y,p,g),P=1/0,T=-1/0,I=[m,_];for(i=2*-Math.PI;i<=2*Math.PI;i+=Math.PI){var O=k+i;1===x?mT&&(T=L)}this.box={minX:w-a,maxX:S+a,minY:P-a,maxY:T+a}}}}),t.exports=v},function(t,e,n){"use strict";e.a=function(t,e){return t=+t,e-=t,function(n){return t+e*n}}},function(t,e,n){var i=n(13),r=Array.prototype.indexOf;t.exports=function(t,e){return!!i(t)&&r.call(t,e)>-1}},function(t,e){t.exports=function(t){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:0,i=this.matrix,r=[t,e,n];return l.transformMat3(r,r,i),r}},{key:"invertMatrix",value:function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,i=this.matrix,r=s.invert([],i),a=[t,e,n];return l.transformMat3(a,a,r),a}},{key:"convert",value:function(t){var e=this.convertPoint(t),n=e.x,i=e.y,r=this.applyMatrix(n,i,1);return{x:r[0],y:r[1]}}},{key:"invert",value:function(t){var e=this.invertMatrix(t.x,t.y,1);return this.invertPoint({x:e[0],y:e[1]})}},{key:"rotate",value:function(t){var e=this.matrix,n=this.center;return s.translate(e,e,[-n.x,-n.y]),s.rotate(e,e,t),s.translate(e,e,[n.x,n.y]),this}},{key:"reflect",value:function(t){switch(t){case"x":this._swapDim("x");break;case"y":this._swapDim("y");break;default:this._swapDim("y")}return this}},{key:"scale",value:function(t,e){var n=this.matrix,i=this.center;return s.translate(n,n,[-i.x,-i.y]),s.scale(n,n,[t,e]),s.translate(n,n,[i.x,i.y]),this}},{key:"translate",value:function(t,e){var n=this.matrix;return s.translate(n,n,[t,e]),this}},{key:"transpose",value:function(){return this.isTransposed=!this.isTransposed,this}}]),t}();t.exports=u},function(t,e,n){var i=n(0),r={splitPoints:function(t){var e=[],n=t.x,r=t.y;return r=i.isArray(r)?r:[r],i.each(r,function(t,r){var a={x:i.isArray(n)?n[r]:n,y:t};e.push(a)}),e},addFillAttrs:function(t,e){e.color&&(t.fill=e.color),i.isNumber(e.opacity)&&(t.opacity=t.fillOpacity=e.opacity)},addStrokeAttrs:function(t,e){e.color&&(t.stroke=e.color),i.isNumber(e.opacity)&&(t.opacity=t.strokeOpacity=e.opacity)}};t.exports=r},function(t,e,n){var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r=n(4);t.exports=function t(e){if("object"!==(void 0===e?"undefined":i(e))||null===e)return e;var n=void 0;if(r(e)){n=[];for(var a=0,o=e.length;an?n:t}},function(t,e,n){var i=n(182);i.translate=function(t,e,n){var r=new Array(9);return i.fromTranslation(r,n),i.multiply(t,r,e)},i.rotate=function(t,e,n){var r=new Array(9);return i.fromRotation(r,n),i.multiply(t,r,e)},i.scale=function(t,e,n){var r=new Array(9);return i.fromScaling(r,n),i.multiply(t,r,e)},t.exports=i},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.setMatrixArrayType=function(t){e.ARRAY_TYPE=r=t},e.toRadian=function(t){return t*a},e.equals=function(t,e){return Math.abs(t-e)<=i*Math.max(1,Math.abs(t),Math.abs(e))};var i=e.EPSILON=1e-6,r=e.ARRAY_TYPE="undefined"!=typeof Float32Array?Float32Array:Array,a=(e.RANDOM=Math.random,Math.PI/180)},function(t,e,n){var i;!function(e){"use strict";function r(){}function a(t,e){for(var n=t.length;n--;)if(t[n].listener===e)return n;return-1}function o(t){return function(){return this[t].apply(this,arguments)}}function s(t){return"function"==typeof t||t instanceof RegExp||!(!t||"object"!=typeof t)&&s(t.listener)}var l=r.prototype,u=e.EventEmitter;l.getListeners=function(t){var e,n,i=this._getEvents();if(t instanceof RegExp){e={};for(n in i)i.hasOwnProperty(n)&&t.test(n)&&(e[n]=i[n])}else e=i[t]||(i[t]=[]);return e},l.flattenListeners=function(t){var e,n=[];for(e=0;e=0&&v=0&&r<=1&&h.push(r);else{var f=u*u-4*l*c;o.isNumberEqual(f,0)?h.push(-u/(2*l)):f>0&&(a=(-u-(s=Math.sqrt(f)))/(2*l),(r=(-u+s)/(2*l))>=0&&r<=1&&h.push(r),a>=0&&a<=1&&h.push(a))}return h},len:function(t,e,n,i,r,s,l,u,c){o.isNil(c)&&(c=1);for(var h=(c=c>1?1:c<0?0:c)/2,f=[-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816],p=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],g=0,d=0;d<12;d++){var v=h*f[d]+h,y=a(v,t,n,r,l),x=a(v,e,i,s,u),m=y*y+x*x;g+=p[d]*Math.sqrt(m)}return h*g}}},function(t,e,n){var i=n(1),r=n(6),a=n(27),o=n(39),s=function t(e){t.superclass.constructor.call(this,e)};s.Symbols={circle:function(t,e,n){return[["M",t,e],["m",-n,0],["a",n,n,0,1,0,2*n,0],["a",n,n,0,1,0,2*-n,0]]},square:function(t,e,n){return[["M",t-n,e-n],["L",t+n,e-n],["L",t+n,e+n],["L",t-n,e+n],["Z"]]},diamond:function(t,e,n){return[["M",t-n,e],["L",t,e-n],["L",t+n,e],["L",t,e+n],["Z"]]},triangle:function(t,e,n){var i=n*Math.sin(1/3*Math.PI);return[["M",t-n,e+i],["L",t,e-i],["L",t+n,e+i],["z"]]},"triangle-down":function(t,e,n){var i=n*Math.sin(1/3*Math.PI);return[["M",t-n,e-i],["L",t+n,e-i],["L",t,e+i],["Z"]]}},s.ATTRS={path:null,lineWidth:1},i.extend(s,r),i.augment(s,{type:"marker",canFill:!0,canStroke:!0,getDefaultAttrs:function(){return{x:0,y:0,lineWidth:1}},calculateBox:function(){var t=this._attrs,e=t.x,n=t.y,i=t.radius,r=this.getHitLineWidth()/2+i;return{minX:e-r,minY:n-r,maxX:e+r,maxY:n+r}},_getPath:function(){var t=this._attrs,e=t.x,n=t.y,r=t.radius||t.r,a=t.symbol||"circle";return(i.isFunction(a)?a:s.Symbols[a])(e,n,r)},createPath:function(t){var e=this._cfg.segments;if(!e||this._cfg.hasUpdate){var n=a.parsePath(this._getPath());t.beginPath();var i;e=[];for(var r=0;r2&&(n.push([i].concat(a.splice(0,2))),o="l",i="m"===i?"l":"L"),"o"===o&&1===a.length&&n.push([i,a[0]]),"r"===o)n.push([i].concat(a));else for(;a.length>=e[o]&&(n.push([i].concat(a.splice(0,e[o]))),e[o]););}),n},f=function(t,e){for(var n=[],i=0,r=t.length;r-2*!e>i;i+=2){var a=[{x:+t[i-2],y:+t[i-1]},{x:+t[i],y:+t[i+1]},{x:+t[i+2],y:+t[i+3]},{x:+t[i+4],y:+t[i+5]}];e?i?r-4===i?a[3]={x:+t[0],y:+t[1]}:r-2===i&&(a[2]={x:+t[0],y:+t[1]},a[3]={x:+t[2],y:+t[3]}):a[0]={x:+t[r-2],y:+t[r-1]}:r-4===i?a[3]=a[2]:i||(a[0]={x:+t[i],y:+t[i+1]}),n.push(["C",(-a[0].x+6*a[1].x+a[2].x)/6,(-a[0].y+6*a[1].y+a[2].y)/6,(a[1].x+6*a[2].x-a[3].x)/6,(a[1].y+6*a[2].y-a[3].y)/6,a[2].x,a[2].y])}return n},p=function(t,e,n,i,r){var a=[];if(null===r&&null===i&&(i=n),t=+t,e=+e,n=+n,i=+i,null!==r){var o=Math.PI/180,s=t+n*Math.cos(-i*o),l=t+n*Math.cos(-r*o);a=[["M",s,e+n*Math.sin(-i*o)],["A",n,n,0,+(r-i>180),0,l,e+n*Math.sin(-r*o)]]}else a=[["M",t,e],["m",0,-i],["a",n,i,0,1,1,0,2*i],["a",n,i,0,1,1,0,-2*i],["z"]];return a},g=function(t){if(!(t=h(t))||!t.length)return[["M",0,0]];var e,n,i=[],r=0,a=0,o=0,s=0,l=0;"M"===t[0][0]&&(o=r=+t[0][1],s=a=+t[0][2],l++,i[0]=["M",r,a]);for(var u,c,g=3===t.length&&"M"===t[0][0]&&"R"===t[1][0].toUpperCase()&&"Z"===t[2][0].toUpperCase(),d=l,v=t.length;d1&&(i*=w=Math.sqrt(w),r*=w);var S=i*i,M=r*r,C=(o===s?-1:1)*Math.sqrt(Math.abs((S*M-S*b*b-M*_*_)/(S*b*b+M*_*_)));g=C*i*b/r+(e+l)/2,d=C*-r*_/i+(n+u)/2,f=Math.asin(((n-d)/r).toFixed(9)),p=Math.asin(((u-d)/r).toFixed(9)),f=ep&&(f-=2*Math.PI),!s&&p>f&&(p-=2*Math.PI)}var A=p-f;if(Math.abs(A)>v){var k=p,P=l,T=u;p=f+v*(s&&p>f?1:-1),x=t(l=g+i*Math.cos(p),u=d+r*Math.sin(p),i,r,a,0,s,P,T,[p,k,g,d])}A=p-f;var I=Math.cos(f),O=Math.sin(f),L=Math.cos(p),E=Math.sin(p),D=Math.tan(A/4),F=4/3*i*D,B=4/3*r*D,R=[e,n],j=[e+F*O,n-B*I],N=[l+F*E,u-B*L],z=[l,u];if(j[0]=2*R[0]-j[0],j[1]=2*R[1]-j[1],c)return[j,N,z].concat(x);for(var Y=[],V=0,X=(x=[j,N,z].concat(x).join().split(",")).length;V7){t[e].shift();for(var a=t[e];a.length;)s[e]="A",r&&(l[e]="A"),t.splice(e++,0,["C"].concat(a.splice(0,6)));t.splice(e,1),n=Math.max(i.length,r&&r.length||0)}},p=function(t,e,a,o,s){t&&e&&"M"===t[s][0]&&"M"!==e[s][0]&&(e.splice(s,0,["M",o.x,o.y]),a.bx=0,a.by=0,a.x=t[s][1],a.y=t[s][2],n=Math.max(i.length,r&&r.length||0))};n=Math.max(i.length,r&&r.length||0);for(var y=0;y1?1:l<0?0:l)/2,c=[-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816],h=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],f=0,p=0;p<12;p++){var g=u*c[p]+u,d=_(g,t,n,r,o),v=_(g,e,i,a,s),y=d*d+v*v;f+=h[p]*Math.sqrt(y)}return u*f},w=function(t,e,n,i,r,a,o,s){if(!(Math.max(t,n)Math.max(r,o)||Math.max(e,i)Math.max(a,s))){var l=(t-n)*(a-s)-(e-i)*(r-o);if(l){var u=((t*i-e*n)*(r-o)-(t-n)*(r*s-a*o))/l,c=((t*i-e*n)*(a-s)-(e-i)*(r*s-a*o))/l,h=+u.toFixed(2),f=+c.toFixed(2);if(!(h<+Math.min(t,n).toFixed(2)||h>+Math.max(t,n).toFixed(2)||h<+Math.min(r,o).toFixed(2)||h>+Math.max(r,o).toFixed(2)||f<+Math.min(e,i).toFixed(2)||f>+Math.max(e,i).toFixed(2)||f<+Math.min(a,s).toFixed(2)||f>+Math.max(a,s).toFixed(2)))return{x:u,y:c}}}},S=function(t,e,n){return e>=t.x&&e<=t.x+t.width&&n>=t.y&&n<=t.y+t.height},M=function(t,e,n,i,r){if(r)return[["M",+t+ +r,e],["l",n-2*r,0],["a",r,r,0,0,1,r,r],["l",0,i-2*r],["a",r,r,0,0,1,-r,r],["l",2*r-n,0],["a",r,r,0,0,1,-r,-r],["l",0,2*r-i],["a",r,r,0,0,1,r,-r],["z"]];var a=[["M",t,e],["l",n,0],["l",0,i],["l",-n,0],["z"]];return a.parsePathArray=m,a},C=function(t,e,n,i){return null===t&&(t=e=n=i=0),null===e&&(e=t.y,n=t.width,i=t.height,t=t.x),{x:t,y:e,width:n,w:n,height:i,h:i,x2:t+n,y2:e+i,cx:t+n/2,cy:e+i/2,r1:Math.min(n,i)/2,r2:Math.max(n,i)/2,r0:Math.sqrt(n*n+i*i)/2,path:M(t,e,n,i),vb:[t,e,n,i].join(" ")}},A=function(t,e,n,i,r,a,o,l){s.isArray(t)||(t=[t,e,n,i,r,a,o,l]);var u=function(t,e,n,i,r,a,o,s){for(var l,u,c,h,f=[],p=[[],[]],g=0;g<2;++g)if(0===g?(u=6*t-12*n+6*r,l=-3*t+9*n-9*r+3*o,c=3*n-3*t):(u=6*e-12*i+6*a,l=-3*e+9*i-9*a+3*s,c=3*i-3*e),Math.abs(l)<1e-12){if(Math.abs(u)<1e-12)continue;(h=-c/u)>0&&h<1&&f.push(h)}else{var d=u*u-4*c*l,v=Math.sqrt(d);if(!(d<0)){var y=(-u+v)/(2*l);y>0&&y<1&&f.push(y);var x=(-u-v)/(2*l);x>0&&x<1&&f.push(x)}}for(var m,_=f.length,b=_;_--;)m=1-(h=f[_]),p[0][_]=m*m*m*t+3*m*m*h*n+3*m*h*h*r+h*h*h*o,p[1][_]=m*m*m*e+3*m*m*h*i+3*m*h*h*a+h*h*h*s;return p[0][b]=t,p[1][b]=e,p[0][b+1]=o,p[1][b+1]=s,p[0].length=p[1].length=b+2,{min:{x:Math.min.apply(0,p[0]),y:Math.min.apply(0,p[1])},max:{x:Math.max.apply(0,p[0]),y:Math.max.apply(0,p[1])}}}.apply(null,t);return C(u.min.x,u.min.y,u.max.x-u.min.x,u.max.y-u.min.y)},k=function(t,e,n,i,r,a,o,s,l){var u=1-l,c=Math.pow(u,3),h=Math.pow(u,2),f=l*l,p=f*l,g=t+2*l*(n-t)+f*(r-2*n+t),d=e+2*l*(i-e)+f*(a-2*i+e),v=n+2*l*(r-n)+f*(o-2*r+n),y=i+2*l*(a-i)+f*(s-2*a+i);return{x:c*t+3*h*l*n+3*u*l*l*r+p*o,y:c*e+3*h*l*i+3*u*l*l*a+p*s,m:{x:g,y:d},n:{x:v,y:y},start:{x:u*t+l*n,y:u*e+l*i},end:{x:u*r+l*o,y:u*a+l*s},alpha:90-180*Math.atan2(g-v,d-y)/Math.PI}},P=function(t,e,n){if(!function(t,e){return t=C(t),e=C(e),S(e,t.x,t.y)||S(e,t.x2,t.y)||S(e,t.x,t.y2)||S(e,t.x2,t.y2)||S(t,e.x,e.y)||S(t,e.x2,e.y)||S(t,e.x,e.y2)||S(t,e.x2,e.y2)||(t.xe.x||e.xt.x)&&(t.ye.y||e.yt.y)}(A(t),A(e)))return n?0:[];for(var i=~~(b.apply(0,t)/8),r=~~(b.apply(0,e)/8),a=[],o=[],s={},l=n?0:[],u=0;u=0&&P<=1&&T>=0&&T<=1&&(n?l++:l.push({x:M.x,y:M.y,t1:P,t2:T}))}}return l},T=function(t,e,n){if(1===n)return[[].concat(t)];var r=[];if("L"===e[0]||"C"===e[0]||"Q"===e[0])r=r.concat(function(t,e,n){var r=[[t[1],t[2]]];n=n||2;var a=[];"A"===e[0]?(r.push(e[6]),r.push(e[7])):"C"===e[0]?(r.push([e[1],e[2]]),r.push([e[3],e[4]]),r.push([e[5],e[6]])):"S"===e[0]||"Q"===e[0]?(r.push([e[1],e[2]]),r.push([e[3],e[4]])):r.push([e[1],e[2]]);for(var o=r,s=1/n,l=0;l=3&&(3===t.length&&e.push("Q"),e=e.concat(t[1])),2===t.length&&e.push("L"),e=e.concat(t[t.length-1])})}(t,e,n));else{var a=[].concat(t);"M"===a[0]&&(a[0]="L");for(var o=0;o<=n-1;o++)r.push(a)}return r},I=function(t,e){if(t.length!==e.length)return!1;var n=!0;return s.each(t,function(t,i){if(t!==e[i])return n=!1,!1}),n};t.exports={parsePathString:h,parsePathArray:m,pathTocurve:y,pathToAbsolute:g,catmullRomToBezier:f,rectPath:M,fillPath:function(t,e){if(1===t.length)return t;var n=t.length-1,i=e.length-1,r=n/i,a=[];if(1===t.length&&"M"===t[0][0]){for(var o=0;o=0;f--)s=o[f].index,"add"===o[f].type?t.splice(s,0,[].concat(t[s])):t.splice(s,1)}var p=a-(i=t.length);if(i0)){t[i]=e[i];break}n=a(n,t[i-1],1)}t[i]=["Q"].concat(n.reduce(function(t,e){return t.concat(e)},[]));break;case"T":t[i]=["T"].concat(n[0]);break;case"C":if(n.length<3){if(!(i>0)){t[i]=e[i];break}n=a(n,t[i-1],2)}t[i]=["C"].concat(n.reduce(function(t,e){return t.concat(e)},[]));break;case"S":if(n.length<2){if(!(i>0)){t[i]=e[i];break}n=a(n,t[i-1],1)}t[i]=["S"].concat(n.reduce(function(t,e){return t.concat(e)},[]));break;default:t[i]=e[i]}return t},intersection:function(t,e){return function(t,e,n){t=y(t),e=y(e);for(var i,r,a,o,s,l,u,c,h,f,p=n?0:[],g=0,d=t.length;g=0&&e._call.call(null,t),e=e._next;--p}function l(){x=(y=_.now())+m,p=g=0;try{s()}finally{p=0,function(){var t,e,n=h,i=1/0;for(;n;)n._call?(i>n._time&&(i=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:h=e);f=t,c(i)}(),x=0}}function u(){var t=_.now(),e=t-y;e>v&&(m-=e,y=t)}function c(t){if(!p){g&&(g=clearTimeout(g));t-x>24?(t<1/0&&(g=setTimeout(l,t-_.now()-m)),d&&(d=clearInterval(d))):(d||(y=_.now(),d=setInterval(u,v)),p=1,b(l))}}e.b=i,e.a=a,e.c=o,e.d=s;var h,f,p=0,g=0,d=0,v=1e3,y=0,x=0,m=0,_="object"==typeof performance&&performance.now?performance:Date,b="object"==typeof window&&window.requestAnimationFrame?window.requestAnimationFrame.bind(window):function(t){setTimeout(t,17)};a.prototype=o.prototype={constructor:a,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?i():+n)+(null==e?0:+e),this._next||f===this||(f?f._next=this:h=this,f=this),this._call=t,this._time=n,c()},stop:function(){this._call&&(this._call=null,this._time=1/0,c())}}},function(t,e,n){"use strict";var i=n(19),r=n(119),a=n(122),o=n(123),s=n(40),l=n(124),u=n(125),c=n(121);e.a=function(t,e){var n,h=typeof e;return null==e||"boolean"===h?Object(c.a)(e):("number"===h?s.a:"string"===h?(n=Object(i.a)(e))?(e=n,r.a):u.a:e instanceof i.a?r.a:e instanceof Date?o.a:Array.isArray(e)?a.a:"function"!=typeof e.valueOf&&"function"!=typeof e.toString||isNaN(e)?l.a:s.a)(t,e)}},function(t,e,n){"use strict";function i(){}function r(t){var e;return t=(t+"").trim().toLowerCase(),(e=_.exec(t))?(e=parseInt(e[1],16),new u(e>>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1)):(e=b.exec(t))?a(parseInt(e[1],16)):(e=w.exec(t))?new u(e[1],e[2],e[3],1):(e=S.exec(t))?new u(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=M.exec(t))?o(e[1],e[2],e[3],e[4]):(e=C.exec(t))?o(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=A.exec(t))?c(e[1],e[2]/100,e[3]/100,1):(e=k.exec(t))?c(e[1],e[2]/100,e[3]/100,e[4]):P.hasOwnProperty(t)?a(P[t]):"transparent"===t?new u(NaN,NaN,NaN,0):null}function a(t){return new u(t>>16&255,t>>8&255,255&t,1)}function o(t,e,n,i){return i<=0&&(t=e=n=NaN),new u(t,e,n,i)}function s(t){return t instanceof i||(t=r(t)),t?(t=t.rgb(),new u(t.r,t.g,t.b,t.opacity)):new u}function l(t,e,n,i){return 1===arguments.length?s(t):new u(t,e,n,null==i?1:i)}function u(t,e,n,i){this.r=+t,this.g=+e,this.b=+n,this.opacity=+i}function c(t,e,n,i){return i<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new f(t,e,n,i)}function h(t,e,n,a){return 1===arguments.length?function(t){if(t instanceof f)return new f(t.h,t.s,t.l,t.opacity);if(t instanceof i||(t=r(t)),!t)return new f;if(t instanceof f)return t;var e=(t=t.rgb()).r/255,n=t.g/255,a=t.b/255,o=Math.min(e,n,a),s=Math.max(e,n,a),l=NaN,u=s-o,c=(s+o)/2;return u?(l=e===s?(n-a)/u+6*(n0&&c<1?0:l,new f(l,u,c,t.opacity)}(t):new f(t,e,n,null==a?1:a)}function f(t,e,n,i){this.h=+t,this.s=+e,this.l=+n,this.opacity=+i}function p(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}e.a=i,n.d(e,"d",function(){return d}),n.d(e,"c",function(){return v}),e.e=r,e.h=s,e.g=l,e.b=u,e.f=h;var g=n(61),d=.7,v=1/d,y="\\s*([+-]?\\d+)\\s*",x="\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*",m="\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*",_=/^#([0-9a-f]{3})$/,b=/^#([0-9a-f]{6})$/,w=new RegExp("^rgb\\("+[y,y,y]+"\\)$"),S=new RegExp("^rgb\\("+[m,m,m]+"\\)$"),M=new RegExp("^rgba\\("+[y,y,y,x]+"\\)$"),C=new RegExp("^rgba\\("+[m,m,m,x]+"\\)$"),A=new RegExp("^hsl\\("+[x,m,m]+"\\)$"),k=new RegExp("^hsla\\("+[x,m,m,x]+"\\)$"),P={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};Object(g.a)(i,r,{displayable:function(){return this.rgb().displayable()},toString:function(){return this.rgb()+""}}),Object(g.a)(u,l,Object(g.b)(i,{brighter:function(t){return t=null==t?v:Math.pow(v,t),new u(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?d:Math.pow(d,t),new u(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return 0<=this.r&&this.r<=255&&0<=this.g&&this.g<=255&&0<=this.b&&this.b<=255&&0<=this.opacity&&this.opacity<=1},toString:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}})),Object(g.a)(f,h,Object(g.b)(i,{brighter:function(t){return t=null==t?v:Math.pow(v,t),new f(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?d:Math.pow(d,t),new f(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,i=n+(n<.5?n:1-n)*e,r=2*n-i;return new u(p(t>=240?t-240:t+120,r,i),p(t,r,i),p(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}))},function(t,e,n){"use strict";e.b=function(t,e){var n=Object.create(t.prototype);for(var i in e)n[i]=e[i];return n},e.a=function(t,e,n){t.prototype=e.prototype=n,n.constructor=t}},function(t,e,n){"use strict";function i(t,e,n,i,r){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*i+o*r)/6}e.a=i,e.b=function(t){var e=t.length-1;return function(n){var r=n<=0?n=0:n>=1?(n=1,e-1):Math.floor(n*e),a=t[r],o=t[r+1],s=r>0?t[r-1]:2*a-o,l=r0&&e.lineToLabel(t)})},n.lineToLabel=function(){},n.getLabelPoint=function(t,e,n){function i(e,n){return o.isArray(e)&&(e=1===t.text.length?e.length<=2?e[e.length-1]:function(t){var e=0;return o.each(t,function(t){e+=t}),e/t.length}(e):e[n]),e}var r=this.get("coord"),a=t.text.length,s={text:t.text[n]};if(e&&"polygon"===this.get("geomType")){var l=function(t,e){for(var n,i,r=-1,a=0,o=0,s=t.length-1,l=0;++ru&&(u=t.x)}),s.x=(s.x+u)/2}"pyramid"===e.shape&&!e.nextPoints&&e.points&&e.points.forEach(function(t){t=r.convert(t),(o.isArray(t.x)&&-1===e.x.indexOf(t.x)||o.isNumber(t.x)&&e.x!==t.x)&&(s.x=(s.x+t.x)/2)}),t.position&&this.setLabelPosition(s,e,n,t.position);var c=this.getLabelOffset(t,n,a);return t.offsetX&&(c.x+=t.offsetX),t.offsetY&&(c.y+=t.offsetY),this.transLabelPoint(s),s.start={x:s.x,y:s.y},s.x+=c.x,s.y+=c.y,s.color=e.color,s},n.setLabelPosition=function(){},n.transLabelPoint=function(t){var e=this.get("coord").applyMatrix(t.x,t.y,1);t.x=e[0],t.y=e[1]},n.getOffsetVector=function(t){var e=t.offset||0,n=this.get("coord");return n.isTransposed?n.applyMatrix(e,0):n.applyMatrix(0,e)},n.getDefaultOffset=function(t){var e=this.get("coord"),n=this.getOffsetVector(t);return e.isTransposed?n[0]:n[1]},n.getLabelOffset=function(t,e,n){var i=this.getDefaultOffset(t),r=this.get("coord").isTransposed,a=r?"x":"y",o=r?1:-1,s={x:0,y:0};return s[a]=e>0||1===n?i*o:i*o*-1,s},n.getLabelAlign=function(t,e,n){var i="center";if(this.get("coord").isTransposed){var r=this.getDefaultOffset(t);i=r<0?"right":0===r?"center":"left",n>1&&0===e&&("right"===i?i="left":"left"===i&&(i="right"))}return i},n._getLabelValue=function(t,e){o.isArray(e)||(e=[e]);var n=[];return o.each(e,function(e){var i=t[e.field];if(o.isArray(i)){var r=[];o.each(i,function(t){r.push(e.getText(t))}),i=r}else i=e.getText(i);(o.isNil(i)||""===i)&&n.push(null),n.push(i)}),n},n._getLabelCfgs=function(t){var e=this,n=this.get("labelCfg"),i=n.scales,r=this.get("label"),a=[];n.globalCfg&&n.globalCfg.type&&e.set("type",n.globalCfg.type),o.each(t,function(t,s){var l={},u=t._origin,c=e._getLabelValue(u,i);if(n.callback){var h=i.map(function(t){return u[t.field]});l=n.callback.apply(null,h)}if(l||0===l){if(o.isString(l)||o.isNumber(l)?l={text:l}:(l.text=l.content||c[0],delete l.content),l=o.mix({},r,n.globalCfg||{},l),t.point=u,l.htmlTemplate&&(l.useHtml=!0,l.text=l.htmlTemplate.call(null,l.text,t,s),delete l.htmlTemplate),l.formatter&&(l.text=l.formatter.call(null,l.text,t,s),delete l.formatter),l.label){var f=l.label;delete l.label,l=o.mix(l,f)}if(l.textStyle){delete l.textStyle.offset;var p=l.textStyle;o.isFunction(p)&&(l.textStyle=p.call(null,l.text,t,s))}l.labelLine&&(l.labelLine=o.mix({},r.labelLine,l.labelLine)),l.textStyle=o.mix({},r.textStyle,l.textStyle),delete l.items,a.push(l)}else a.push(null)}),this.set("labelItemCfgs",a)},n.showLabels=function(t,e){var n=this.get("labelRenderer"),i=this.getLabelsItems(t,e);e=[].concat(e);var r=this.get("type");i=this.adjustItems(i,e),this.drawLines(i),n.set("items",i.filter(function(t,n){return!!t||(e.splice(n,1),!1)})),r&&(n.set("shapes",e),n.set("type",r),n.set("points",t)),n.set("canvas",this.get("canvas")),n.draw()},n.destroy=function(){this.get("labelRenderer").destroy(),t.prototype.destroy.call(this)},e}(i);t.exports=l},function(t,e,n){function i(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var r=n(53),a=n(3),o=function(t){function e(e){var n,r=i(i(n=t.call(this)||this)),o={visible:!0},s=r.getDefaultCfg();return r._attrs=o,a.deepMix(o,s,e),n}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){return{}},n.get=function(t){return this._attrs[t]},n.set=function(t,e){this._attrs[t]=e},n.changeVisible=function(){},n.destroy=function(){this._attrs={},this.removeAllListeners(),this.destroyed=!0},e}(r);t.exports=o},function(t,e,n){var i=n(3),r=n(158),a=n(327),o=n(14).FONT_FAMILY,s=i.Event,l=i.Group,u=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{type:"continuous-legend",items:null,layout:"vertical",width:20,height:156,textStyle:{fill:"#333",textAlign:"center",textBaseline:"middle",stroke:"#fff",lineWidth:5,fontFamily:o},hoverTextStyle:{fill:"rgba(0,0,0,0.25)"},slidable:!0,triggerAttr:{fill:"#fff",shadowBlur:10,shadowColor:"rgba(0,0,0,0.65)",radius:2},_range:[0,100],middleBackgroundStyle:{fill:"#D9D9D9"},textOffset:4,lineStyle:{lineWidth:1,stroke:"#fff"},pointerStyle:{fill:"rgb(230, 230, 230)"}})},n._calStartPoint=function(){var t={x:10,y:this.get("titleGap")-8},e=this.get("titleShape");if(e){var n=e.getBBox();t.y+=n.height}return t},n.beforeRender=function(){var e=this.get("items");i.isArray(e)&&!i.isEmpty(e)&&(t.prototype.beforeRender.call(this),this.set("firstItem",e[0]),this.set("lastItem",e[e.length-1]))},n._formatItemValue=function(t){var e=this.get("formatter")||this.get("itemFormatter");return e&&(t=e.call(this,t)),t},n.render=function(){t.prototype.render.call(this),this.get("slidable")?this._renderSlider():this._renderUnslidable()},n._renderSlider=function(){var t=new l,e=new l,n=new l,i=this._calStartPoint(),r=this.get("group").addGroup(a,{minHandleElement:t,maxHandleElement:e,backgroundElement:n,layout:this.get("layout"),range:this.get("_range"),width:this.get("width"),height:this.get("height")});r.translate(i.x,i.y),this.set("slider",r);this._renderSliderShape().attr("clip",r.get("middleHandleElement")),this._renderTrigger()},n._addMiddleBar=function(t,e,n){return t.addShape(e,{attrs:i.mix({},n,this.get("middleBackgroundStyle"))}),t.addShape(e,{attrs:n})},n._renderTrigger=function(){var t=this.get("firstItem"),e=this.get("lastItem"),n=this.get("layout"),r=this.get("textStyle"),a=this.get("triggerAttr"),o=i.mix({},a),s=i.mix({},a),l=i.mix({text:this._formatItemValue(t.value)+""},r),u=i.mix({text:this._formatItemValue(e.value)+""},r);"vertical"===n?(this._addVerticalTrigger("min",o,l),this._addVerticalTrigger("max",s,u)):(this._addHorizontalTrigger("min",o,l),this._addHorizontalTrigger("max",s,u))},n._addVerticalTrigger=function(t,e,n){var r=this.get("slider").get(t+"HandleElement"),a=this.get("width"),o=r.addShape("rect",{attrs:i.mix({x:a/2-8-2,y:"min"===t?0:-8,width:18,height:8},e)}),s=r.addShape("text",{attrs:i.mix(n,{x:a+this.get("textOffset"),y:"max"===t?-4:4,textAlign:"start",lineHeight:1,textBaseline:"middle"})}),l="vertical"===this.get("layout")?"ns-resize":"ew-resize";o.attr("cursor",l),s.attr("cursor",l),this.set(t+"ButtonElement",o),this.set(t+"TextElement",s)},n._addHorizontalTrigger=function(t,e,n){var r=this.get("slider").get(t+"HandleElement"),a=r.addShape("rect",{attrs:i.mix({x:"min"===t?-8:0,y:-8-this.get("height")/2,width:8,height:16},e)}),o=r.addShape("text",{attrs:i.mix(n,{x:"min"===t?-12:12,y:4+this.get("textOffset")+10,textAlign:"min"===t?"end":"start",textBaseline:"middle"})}),s="vertical"===this.get("layout")?"ns-resize":"ew-resize";a.attr("cursor",s),o.attr("cursor",s),this.set(t+"ButtonElement",a),this.set(t+"TextElement",o)},n._bindEvents=function(){var t=this;if(this.get("slidable")){this.get("slider").on("sliderchange",function(e){var n=e.range,i=t.get("firstItem").value,r=t.get("lastItem").value,a=i+n[0]/100*(r-i),o=i+n[1]/100*(r-i);t._updateElement(a,o);var l=new s("itemfilter",e,!0,!0);l.range=[a,o],t.emit("itemfilter",l)})}this.get("hoverable")&&(this.get("group").on("mousemove",i.wrapBehavior(this,"_onMouseMove")),this.get("group").on("mouseleave",i.wrapBehavior(this,"_onMouseLeave")))},n._updateElement=function(t,e){var n=this.get("minTextElement"),i=this.get("maxTextElement");e>1&&(t=parseInt(t,10),e=parseInt(e,10)),n.attr("text",this._formatItemValue(t)+""),i.attr("text",this._formatItemValue(e)+"")},n._onMouseLeave=function(){var t=this.get("group").findById("hoverPointer");t&&t.destroy();var e=this.get("group").findById("hoverText");e&&e.destroy(),this.get("canvas").draw()},n._onMouseMove=function(t){var e,n=this.get("height"),i=this.get("width"),r=this.get("items"),a=this.get("canvas").get("el").getBoundingClientRect(),o=this.get("group").getBBox();if("vertical"===this.get("layout")){var s=5;"color-legend"===this.get("type")&&(s=30);var l=this.get("titleGap"),u=this.get("titleShape");u&&(l+=u.getBBox().maxY-u.getBBox().minY);var c=t.clientY||t.event.clientY;c=c-a.y-this.get("group").attr("matrix")[7]+o.y-s+l,e=r[0].value+(1-c/n)*(r[r.length-1].value-r[0].value)}else{var h=t.clientX||t.event.clientX;h=h-a.x-this.get("group").attr("matrix")[6],e=r[0].value+h/i*(r[r.length-1].value-r[0].value)}e=e.toFixed(2),this.activate(e),this.emit("mousehover",{value:e})},n.activate=function(t){if(t){var e=this.get("group").findById("hoverPointer"),n=this.get("group").findById("hoverText"),r=this.get("items");if(!(tr[r.length-1].value)){var a,o=this.get("height"),s=this.get("width"),l=this.get("titleShape"),u=this.get("titleGap"),c=[],h=(t-r[0].value)/(r[r.length-1].value-r[0].value);if("vertical"===this.get("layout")){var f=0,p=0;"color-legend"===this.get("type")&&(f=u,l&&(f+=l.getBBox().height)),this.get("slidable")&&("color-legend"===this.get("type")?f-=13:(f=u-15,l&&(f+=l.getBBox().height)),p+=10),c=[[p,(h=(1-h)*o)+f],[p-10,h+f-5],[p-10,h+f+5]],a=i.mix({},{x:s+this.get("textOffset")/2+p,y:h+f,text:this._formatItemValue(t)+""},this.get("textStyle"),{textAlign:"start"})}else{var g=0,d=0;"color-legend"===this.get("type")&&(g=u,l&&(g+=l.getBBox().height)),this.get("slidable")&&("color-legend"===this.get("type")?g-=7:(g=u,l||(g-=7)),d+=10),c=[[(h*=s)+d,g],[h+d-5,g-10],[h+d+5,g-10]],a=i.mix({},{x:h-5,y:o+this.get("textOffset")+g,text:this._formatItemValue(t)+""},this.get("textStyle"))}var v=i.mix(a,this.get("hoverTextStyle"));n?n.attr(v):(n=this.get("group").addShape("text",{attrs:v})).set("id","hoverText"),e?e.attr(i.mix({points:c},this.get("pointerStyle"))):(e=this.get("group").addShape("Polygon",{attrs:i.mix({points:c},this.get("pointerStyle"))})).set("id","hoverPointer"),this.get("canvas").draw()}}},n.deactivate=function(){var t=this.get("group").findById("hoverPointer");t&&t.destroy();var e=this.get("group").findById("hoverText");e&&e.destroy(),this.get("canvas").draw()},e}(r);t.exports=u},function(t,e,n){var i=n(66),r=n(3),a=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return r.mix({},e,{x:0,y:0,items:null,titleContent:null,showTitle:!0,plotRange:null,offset:10,timeStamp:0,inPlot:!0,crosshairs:null})},n.isContentChange=function(t,e){var n=this.get("titleContent"),i=this.get("items"),a=!(t===n&&i.length===e.length);return a||r.each(e,function(t,e){var n=i[e];for(var o in t)if(t.hasOwnProperty(o)&&!r.isObject(t[o])&&t[o]!==n[o]){a=!0;break}if(a)return!1}),a},n.setContent=function(t,e){var n=(new Date).valueOf();return this.set("items",e),this.set("titleContent",t),this.set("timeStamp",n),this.render(),this},n.setPosition=function(t,e){this.set("x",t),this.set("y",e)},n.render=function(){},n.clear=function(){},n.show=function(){this.set("visible",!0)},n.hide=function(){this.set("visible",!1)},n.destroy=function(){},e}(i);t.exports=a},function(t,e,n){"use strict";function i(t,e){this._groups=t,this._parents=e}function r(){return new i([[document.documentElement]],F)}n.d(e,"c",function(){return F}),e.a=i;var a=n(402),o=n(403),s=n(404),l=n(405),u=n(382),c=n(407),h=n(408),f=n(409),p=n(410),g=n(411),d=n(412),v=n(413),y=n(414),x=n(415),m=n(416),_=n(417),b=n(384),w=n(418),S=n(419),M=n(420),C=n(421),A=n(422),k=n(423),P=n(424),T=n(425),I=n(426),O=n(427),L=n(428),E=n(374),D=n(429),F=[null];i.prototype=r.prototype={constructor:i,select:a.a,selectAll:o.a,filter:s.a,data:l.a,enter:u.b,exit:c.a,merge:h.a,order:f.a,sort:p.a,call:g.a,nodes:d.a,node:v.a,size:y.a,empty:x.a,each:m.a,attr:_.a,style:b.a,property:w.a,classed:S.a,text:M.a,html:C.a,raise:A.a,lower:k.a,append:P.a,insert:T.a,remove:I.a,clone:O.a,datum:L.a,on:E.b,dispatch:D.a},e.b=r},function(t,e,n){"use strict";function i(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}n.d(e,"c",function(){return u}),n.d(e,"d",function(){return c}),n.d(e,"b",function(){return p}),n.d(e,"a",function(){return g}),e.g=function(t,e){var n=i(t,e);if(n.state>l)throw new Error("too late; already scheduled");return n},e.h=function(t,e){var n=i(t,e);if(n.state>c)throw new Error("too late; already started");return n},e.f=i;var r=n(438),a=n(170),o=Object(r.a)("start","end","interrupt"),s=[],l=0,u=1,c=2,h=3,f=4,p=5,g=6;e.e=function(t,e,n,i,r,d){var v=t.__transition;if(v){if(n in v)return}else t.__transition={};!function(t,e,n){function i(p){var d,v,y,x;if(n.state!==u)return o();for(d in l)if((x=l[d]).name===n.name){if(x.state===h)return Object(a.timeout)(i);x.state===f?(x.state=g,x.timer.stop(),x.on.call("interrupt",t,t.__data__,x.index,x.group),delete l[d]):+d0?new Date(t).getTime():new Date(t.replace(/-/gi,"/")).getTime()),r(t)&&(t=t.getTime()),t}}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=n(401);n.d(e,"create",function(){return i.a});var r=n(360);n.d(e,"creator",function(){return r.a});var a=n(430);n.d(e,"local",function(){return a.a});var o=n(381);n.d(e,"matcher",function(){return o.a});var s=n(431);n.d(e,"mouse",function(){return s.a});var l=n(370);n.d(e,"namespace",function(){return l.a});var u=n(371);n.d(e,"namespaces",function(){return u.a});var c=n(361);n.d(e,"clientPoint",function(){return c.a});var h=n(379);n.d(e,"select",function(){return h.a});var f=n(432);n.d(e,"selectAll",function(){return f.a});var p=n(69);n.d(e,"selection",function(){return p.b});var g=n(372);n.d(e,"selector",function(){return g.a});var d=n(380);n.d(e,"selectorAll",function(){return d.a});var v=n(384);n.d(e,"style",function(){return v.b});var y=n(433);n.d(e,"touch",function(){return y.a});var x=n(434);n.d(e,"touches",function(){return x.a});var m=n(373);n.d(e,"window",function(){return m.a});var _=n(374);n.d(e,"event",function(){return _.c}),n.d(e,"customEvent",function(){return _.a})},function(t,e,n){t.exports={Position:n(292),Color:n(293),Shape:n(294),Size:n(295),Opacity:n(296),ColorUtil:n(149)}},function(t,e,n){var i=n(75),r=n(17);r.Linear=n(33),r.Identity=n(175),r.Cat=n(77),r.Time=n(176),r.TimeCat=n(178),r.Log=n(179),r.Pow=n(180);var a=function(t){if(r.hasOwnProperty(t)){var e=i(t);r[e]=function(e){return new r[t](e)}}};for(var o in r)a(o);var s=["cat","timeCat"];r.isCategory=function(t){return s.indexOf(t)>=0},t.exports=r},function(t,e,n){var i=n(23);t.exports=function(t){var e=i(t);return e.charAt(0).toLowerCase()+e.substring(1)}},function(t,e){function n(t,e){var n=t.length;if(0===n)return NaN;var i=t[0];if(e=t[n-1])return t[n-1];for(var r=1;rt[n-1])return NaN;if(er&&(e=parseFloat(e.toFixed(n)))}else for(;t>10;)e*=10,t/=10;return e}(t*=i);i*=o,t/=o}var s=(t="floor"===n?a.snapFloor(e,t):"ceil"===n?a.snapCeiling(e,t):a.snapTo(e,t))*i;if(Math.abs(i)<1&&s.toString().length>r){s=t/parseInt(1/i)*(i>0?1:-1)}return s},snapMultiple:function(t,e,n){return("ceil"===n?Math.ceil(t/e):"floor"===n?Math.floor(t/e):Math.round(t/e))*e},snapTo:function(t,e){var r=n(t,e),a=i(t,e);if(isNaN(r)||isNaN(a)){if(t[0]>=e)return t[0];var o=t[t.length-1];if(o<=e)return o}return Math.abs(e-r)20&&(r=20),parseFloat(t.toFixed(r))}};t.exports=a},function(t,e,n){var i=n(17),r=n(78),a=n(2),o=n(9),s=n(10),l=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n._initDefaultCfg=function(){t.prototype._initDefaultCfg.call(this),this.type="cat",this.isCategory=!0,this.isRounding=!0},n.init=function(){var t=this.values,e=this.tickCount;if(a(t,function(e,n){t[n]=e.toString()}),!this.ticks){var n=t;if(e){n=r({maxCount:e,data:t,isRounding:this.isRounding}).ticks}this.ticks=n}},n.getText=function(e){return-1===this.values.indexOf(e)&&o(e)&&(e=this.values[Math.round(e)]),t.prototype.getText.call(this,e)},n.translate=function(t){var e=this.values.indexOf(t);return-1===e&&o(t)?e=t:-1===e&&(e=NaN),e},n.scale=function(t){var e,n=this.rangeMin(),i=this.rangeMax();return(s(t)||-1!==this.values.indexOf(t))&&(t=this.translate(t)),e=this.values.length>1?t/(this.values.length-1):t,n+e*(i-n)},n.invert=function(t){if(s(t))return t;var e=this.rangeMin(),n=this.rangeMax();tn&&(t=n);var i=(t-e)/(n-e),r=Math.round(i*(this.values.length-1))%this.values.length;return r=r||0,this.values[r]},e}(i);i.Cat=l,t.exports=l},function(t,e,n){var i=n(2);t.exports=function(t){var e,n={},r=[],a=t.isRounding,o=function(t){var e=[];return i(t,function(t){e=e.concat(t)}),e}(t.data),s=o.length,l=t.maxCount||8;if(a?2===(e=function(t,e){var n;for(n=e;n>0&&t%n!=0;n--);if(1===n)for(n=e;n>0&&(t-1)%n!=0;n--);return n}(s-1,l-1)+1)?e=l:e3?0:(t-t%10!=10)*t%10]}};var x={D:function(t){return t.getDate()},DD:function(t){return s(t.getDate())},Do:function(t,e){return e.DoFn(t.getDate())},d:function(t){return t.getDay()},dd:function(t){return s(t.getDay())},ddd:function(t,e){return e.dayNamesShort[t.getDay()]},dddd:function(t,e){return e.dayNames[t.getDay()]},M:function(t){return t.getMonth()+1},MM:function(t){return s(t.getMonth()+1)},MMM:function(t,e){return e.monthNamesShort[t.getMonth()]},MMMM:function(t,e){return e.monthNames[t.getMonth()]},YY:function(t){return String(t.getFullYear()).substr(2)},YYYY:function(t){return s(t.getFullYear(),4)},h:function(t){return t.getHours()%12||12},hh:function(t){return s(t.getHours()%12||12)},H:function(t){return t.getHours()},HH:function(t){return s(t.getHours())},m:function(t){return t.getMinutes()},mm:function(t){return s(t.getMinutes())},s:function(t){return t.getSeconds()},ss:function(t){return s(t.getSeconds())},S:function(t){return Math.round(t.getMilliseconds()/100)},SS:function(t){return s(Math.round(t.getMilliseconds()/10),2)},SSS:function(t){return s(t.getMilliseconds(),3)},a:function(t,e){return t.getHours()<12?e.amPm[0]:e.amPm[1]},A:function(t,e){return t.getHours()<12?e.amPm[0].toUpperCase():e.amPm[1].toUpperCase()},ZZ:function(t){var e=t.getTimezoneOffset();return(e>0?"-":"+")+s(100*Math.floor(Math.abs(e)/60)+Math.abs(e)%60,4)}},m={D:[c,function(t,e){t.day=e}],Do:[new RegExp(c.source+h.source),function(t,e){t.day=parseInt(e,10)}],M:[c,function(t,e){t.month=e-1}],YY:[c,function(t,e){var n=+(""+(new Date).getFullYear()).substr(0,2);t.year=""+(e>68?n-1:n)+e}],h:[c,function(t,e){t.hour=e}],m:[c,function(t,e){t.minute=e}],s:[c,function(t,e){t.second=e}],YYYY:[/\d{4}/,function(t,e){t.year=e}],S:[/\d/,function(t,e){t.millisecond=100*e}],SS:[/\d{2}/,function(t,e){t.millisecond=10*e}],SSS:[/\d{3}/,function(t,e){t.millisecond=e}],d:[c,p],ddd:[h,p],MMM:[h,o("monthNamesShort")],MMMM:[h,o("monthNames")],a:[h,function(t,e,n){var i=e.toLowerCase();i===n.amPm[0]?t.isPm=!1:i===n.amPm[1]&&(t.isPm=!0)}],ZZ:[/([\+\-]\d\d:?\d\d|Z)/,function(t,e){"Z"===e&&(e="+00:00");var n,i=(e+"").match(/([\+\-]|\d\d)/gi);i&&(n=60*i[1]+parseInt(i[2],10),t.timezoneOffset="+"===i[0]?n:-n)}]};m.dd=m.d,m.dddd=m.ddd,m.DD=m.D,m.mm=m.m,m.hh=m.H=m.HH=m.h,m.MM=m.M,m.ss=m.s,m.A=m.a,l.masks={default:"ddd MMM DD YYYY HH:mm:ss",shortDate:"M/D/YY",mediumDate:"MMM D, YYYY",longDate:"MMMM D, YYYY",fullDate:"dddd, MMMM D, YYYY",shortTime:"HH:mm",mediumTime:"HH:mm:ss",longTime:"HH:mm:ss.SSS"},l.format=function(t,e,n){var i=n||l.i18n;if("number"==typeof t&&(t=new Date(t)),"[object Date]"!==Object.prototype.toString.call(t)||isNaN(t.getTime()))throw new Error("Invalid Date in fecha.format");var r=[];return e=(e=l.masks[e]||e||l.masks.default).replace(f,function(t,e){return r.push(e),"??"}),(e=e.replace(u,function(e){return e in x?x[e](t,i):e.slice(1,e.length-1)})).replace(/\?\?/g,function(){return r.shift()})},l.parse=function(t,e,n){var i=n||l.i18n;if("string"!=typeof e)throw new Error("Invalid format in fecha.parse");if(e=l.masks[e]||e,t.length>1e3)return!1;var r=!0,a={};if(e.replace(u,function(e){if(m[e]){var n=m[e],o=t.search(n[0]);~o?t.replace(n[0],function(e){return n[1](a,e,i),t=t.substr(o+e.length),e}):r=!1}return m[e]?"":e.slice(1,e.length-1)}),!r)return!1;var o=new Date;!0===a.isPm&&null!=a.hour&&12!=+a.hour?a.hour=+a.hour+12:!1===a.isPm&&12==+a.hour&&(a.hour=0);var s;return null!=a.timezoneOffset?(a.minute=+(a.minute||0)-+a.timezoneOffset,s=new Date(Date.UTC(a.year||o.getFullYear(),a.month||0,a.day||1,a.hour||0,a.minute||0,a.second||0,a.millisecond||0))):s=new Date(a.year||o.getFullYear(),a.month||0,a.day||1,a.hour||0,a.minute||0,a.second||0,a.millisecond||0),s},void 0!==t&&t.exports?t.exports=l:void 0!==(i=function(){return l}.call(e,n,e,t))&&(t.exports=i)}()},function(t,e,n){var i=n(12);t.exports=function(t){return i(t,"Date")}},function(t,e,n){t.exports={isFunction:n(11),isObject:n(24),isBoolean:n(82),isNil:n(5),isString:n(10),isArray:n(4),isNumber:n(9),isEmpty:n(83),uniqueId:n(86),clone:n(46),deepMix:n(47),assign:n(8),merge:n(47),upperFirst:n(87),each:n(2),isEqual:n(49),toArray:n(34),extend:n(88),augment:n(89),remove:n(90),isNumberEqual:n(35),toRadian:n(91),toDegree:n(92),mod:n(93),clamp:n(50),createDom:n(94),modifyCSS:n(95),requestAnimationFrame:n(96),getRatio:function(){return window.devicePixelRatio?window.devicePixelRatio:2},mat3:n(51),vec2:n(97),vec3:n(98),transform:n(99)}},function(t,e,n){var i=n(12);t.exports=function(t){return i(t,"Boolean")}},function(t,e,n){var i=n(5),r=n(13),a=n(84),o=n(85),s=Object.prototype.hasOwnProperty;t.exports=function(t){if(i(t))return!0;if(r(t))return!t.length;var e=a(t);if("Map"===e||"Set"===e)return!t.size;if(o(t))return!Object.keys(t).length;for(var n in t)if(s.call(t,n))return!1;return!0}},function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).replace(/^\[object /,"").replace(/\]$/,"")}},function(t,e){var n=Object.prototype;t.exports=function(t){var e=t&&t.constructor;return t===("function"==typeof e&&e.prototype||n)}},function(t,e){var n=function(){var t={};return function(e){return e=e||"g",t[e]?t[e]+=1:t[e]=1,e+t[e]}}();t.exports=n},function(t,e,n){var i=n(23);t.exports=function(t){var e=i(t);return e.charAt(0).toUpperCase()+e.substring(1)}},function(t,e,n){var i=n(11),r=n(8);t.exports=function(t,e,n,a){i(e)||(n=e,e=t,t=function(){});var o=Object.create?function(t,e){return Object.create(t,{constructor:{value:e}})}:function(t,e){function n(){}n.prototype=t;var i=new n;return i.constructor=e,i},s=o(e.prototype,t);return t.prototype=r(s,t.prototype),t.superclass=o(e.prototype,e),r(s,n),r(t,a),t}},function(t,e,n){var i=n(11),r=n(34),a=n(8);t.exports=function(t){for(var e=r(arguments),n=1;n-1;)i.call(t,s,1);return t}},function(t,e){var n=Math.PI/180;t.exports=function(t){return n*t}},function(t,e){var n=180/Math.PI;t.exports=function(t){return n*t}},function(t,e){t.exports=function(t,e){return(t%e+e)%e}},function(t,e){var n=document.createElement("table"),i=document.createElement("tr"),r=/^\s*<(\w+|!)[^>]*>/,a={tr:document.createElement("tbody"),tbody:n,thead:n,tfoot:n,td:i,th:i,"*":document.createElement("div")};t.exports=function(t){var e=r.test(t)&&RegExp.$1;e in a||(e="*");var n=a[e];t=t.replace(/(^\s*)|(\s*$)/g,""),n.innerHTML=""+t;var i=n.childNodes[0];return n.removeChild(i),i}},function(t,e){t.exports=function(t,e){if(t)for(var n in e)e.hasOwnProperty(n)&&(t.style[n]=e[n]);return t}},function(t,e){t.exports=function(t){return(window.requestAnimationFrame||window.webkitRequestAnimationFrame||function(t){return setTimeout(t,16)})(t)}},function(t,e,n){var i=n(183),r=n(50);i.angle=function(t,e){var n=i.dot(t,e)/(i.length(t)*i.length(e));return Math.acos(r(n,-1,1))},i.direction=function(t,e){return t[0]*e[1]-e[0]*t[1]},i.angleTo=function(t,e,n){var r=i.angle(t,e),a=i.direction(t,e)>=0;return n?a?2*Math.PI-r:r:a?r:2*Math.PI-r},i.vertical=function(t,e,n){return n?(t[0]=e[1],t[1]=-1*e[0]):(t[0]=-1*e[1],t[1]=e[0]),t},t.exports=i},function(t,e,n){var i=n(184);t.exports=i},function(t,e,n){var i=n(46),r=n(2),a=n(51);t.exports=function(t,e){return t=i(t),r(e,function(e){switch(e[0]){case"t":a.translate(t,t,[e[1],e[2]]);break;case"s":a.scale(t,t,[e[1],e[2]]);break;case"r":a.rotate(t,t,e[1]);break;case"m":a.multiply(t,t,e[1]);break;default:return!1}}),t}},function(t,e,n){var i=n(1),r=function(t,e,n,i){this.type=t,this.target=null,this.currentTarget=null,this.bubbles=n,this.cancelable=i,this.timeStamp=(new Date).getTime(),this.defaultPrevented=!1,this.propagationStopped=!1,this.removed=!1,this.event=e};i.augment(r,{preventDefault:function(){this.defaultPrevented=this.cancelable&&!0},stopPropagation:function(){this.propagationStopped=!0},remove:function(){this.remove=!0},clone:function(){return i.clone(this)},toString:function(){return"[Event (type="+this.type+")]"}}),t.exports=r},function(t,e,n){function i(t,e,n){for(var i,r=t.length-1;r>=0;r--){var a=t[r];if(a._cfg.visible&&a._cfg.capture&&(a.isGroup?i=a.getShape(e,n):a.isHit(e,n)&&(i=a)),i)break}return i}function r(t){if(!t._cfg&&t!==c){var e=t.superclass.constructor;e&&!e._cfg&&r(e),t._cfg={},a.merge(t._cfg,e._cfg),a.merge(t._cfg,t.CFG)}}var a=n(1),o=n(102),s=n(188),l={},u="_INDEX",c=function t(e){t.superclass.constructor.call(this,e),this.set("children",[]),this.set("tobeRemoved",[]),this._beforeRenderUI(),this._renderUI(),this._bindUI()};a.extend(c,o),a.augment(c,{isGroup:!0,type:"group",canFill:!0,canStroke:!0,getDefaultCfg:function(){return r(this.constructor),a.merge({},this.constructor._cfg)},_beforeRenderUI:function(){},_renderUI:function(){},_bindUI:function(){},addShape:function(t,e){var n=this.get("canvas");e=e||{};var i=l[t];if(i||(i=a.upperFirst(t),l[t]=i),e.attrs&&n){var r=e.attrs;if("text"===t){var o=n.get("fontFamily");o&&(r.fontFamily=r.fontFamily?r.fontFamily:o)}}e.canvas=n,e.type=t;var u=new s[i](e);return this.add(u),u},addGroup:function(t,e){var n,i=this.get("canvas");if(e=a.merge({},e),a.isFunction(t))e?(e.canvas=i,e.parent=this,n=new t(e)):n=new t({canvas:i,parent:this}),this.add(n);else if(a.isObject(t))t.canvas=i,n=new c(t),this.add(n);else{if(void 0!==t)return!1;n=new c,this.add(n)}return n},renderBack:function(t,e){var n=this.get("backShape"),i=this.getBBox();return a.merge(e,{x:i.minX-t[3],y:i.minY-t[0],width:i.width+t[1]+t[3],height:i.height+t[0]+t[2]}),n?n.attr(e):n=this.addShape("rect",{zIndex:-1,attrs:e}),this.set("backShape",n),this.sort(),n},removeChild:function(t,e){if(arguments.length>=2)this.contain(t)&&t.remove(e);else{if(1===arguments.length){if(!a.isBoolean(t))return this.contain(t)&&t.remove(!0),this;e=t}0===arguments.length&&(e=!0),c.superclass.remove.call(this,e)}return this},add:function(t){var e=this,n=e.get("children");if(a.isArray(t))a.each(t,function(t){var n=t.get("parent");n&&n.removeChild(t,!1),e._setCfgProperty(t)}),e._cfg.children=n.concat(t);else{var i=t,r=i.get("parent");r&&r.removeChild(i,!1),e._setCfgProperty(i),n.push(i)}return e},_setCfgProperty:function(t){var e=this._cfg;t.set("parent",this),t.set("canvas",e.canvas),e.timeline&&t.set("timeline",e.timeline)},contain:function(t){return this.get("children").indexOf(t)>-1},getChildByIndex:function(t){return this.get("children")[t]},getFirst:function(){return this.getChildByIndex(0)},getLast:function(){var t=this.get("children").length-1;return this.getChildByIndex(t)},getBBox:function(){var t=1/0,e=-1/0,n=1/0,i=-1/0,r=this.get("children");r.length>0?a.each(r,function(r){if(r.get("visible")){if(r.isGroup&&0===r.get("children").length)return;var a=r.getBBox();if(!a)return!0;var o=[a.minX,a.minY,1],s=[a.minX,a.maxY,1],l=[a.maxX,a.minY,1],u=[a.maxX,a.maxY,1];r.apply(o),r.apply(s),r.apply(l),r.apply(u);var c=Math.min(o[0],s[0],l[0],u[0]),h=Math.max(o[0],s[0],l[0],u[0]),f=Math.min(o[1],s[1],l[1],u[1]),p=Math.max(o[1],s[1],l[1],u[1]);ce&&(e=h),fi&&(i=p)}}):(t=0,e=0,n=0,i=0);var o={minX:t,minY:n,maxX:e,maxY:i};return o.x=o.minX,o.y=o.minY,o.width=o.maxX-o.minX,o.height=o.maxY-o.minY,o},getCount:function(){return this.get("children").length},sort:function(){var t=this.get("children");return a.each(t,function(t,e){return t[u]=e,t}),t.sort(function(t){return function(e,n){var i=t(e,n);return 0===i?e[u]-n[u]:i}}(function(t,e){return t.get("zIndex")-e.get("zIndex")})),this},findById:function(t){return this.find(function(e){return e.get("id")===t})},find:function(t){if(a.isString(t))return this.findById(t);var e=this.get("children"),n=null;return a.each(e,function(e){if(t(e)?n=e:e.find&&(n=e.find(t)),n)return!1}),n},findAll:function(t){var e=this.get("children"),n=[],i=[];return a.each(e,function(e){t(e)&&n.push(e),e.findAllBy&&(i=e.findAllBy(t),n=n.concat(i))}),n},findBy:function(t){var e=this.get("children"),n=null;return a.each(e,function(e){if(t(e)?n=e:e.findBy&&(n=e.findBy(t)),n)return!1}),n},findAllBy:function(t){var e=this.get("children"),n=[],i=[];return a.each(e,function(e){t(e)&&n.push(e),e.findAllBy&&(i=e.findAllBy(t),n=n.concat(i))}),n},getShape:function(t,e){var n,r=this._attrs.clip,a=this._cfg.children;if(r){var o=[t,e,1];r.invert(o,this.get("canvas")),r.isPointInPath(o[0],o[1])&&(n=i(a,t,e))}else n=i(a,t,e);return n},clearTotalMatrix:function(){if(this.get("totalMatrix")){this.setSilent("totalMatrix",null);for(var t=this._cfg.children,e=0;e=0;n--)e[n].remove(!0,t);return this._cfg.children=[],this},destroy:function(){this.get("destroyed")||(this.clear(),c.superclass.destroy.call(this))},clone:function(){var t=this._cfg.children,e=new c;return a.each(t,function(t){e.add(t.clone())}),e}}),t.exports=c},function(t,e,n){var i=n(1),r=n(185),a=n(186),o=n(187),s=n(53),l=function(t){this._cfg={zIndex:0,capture:!0,visible:!0,destroyed:!1},i.assign(this._cfg,this.getDefaultCfg(),t),this.initAttrs(this._cfg.attrs),this._cfg.attrs={},this.initTransform(),this.init()};l.CFG={id:null,zIndex:0,canvas:null,parent:null,capture:!0,context:null,visible:!0,destroyed:!1},i.augment(l,r,a,s,o,{init:function(){this.setSilent("animable",!0),this.setSilent("animating",!1)},getParent:function(){return this._cfg.parent},getDefaultCfg:function(){return{}},set:function(t,e){return"zIndex"===t&&this._beforeSetZIndex&&this._beforeSetZIndex(e),"loading"===t&&this._beforeSetLoading&&this._beforeSetLoading(e),this._cfg[t]=e,this},setSilent:function(t,e){this._cfg[t]=e},get:function(t){return this._cfg[t]},show:function(){return this._cfg.visible=!0,this},hide:function(){return this._cfg.visible=!1,this},remove:function(t,e){var n=this._cfg,r=n.parent,a=n.el;return r&&i.remove(r.get("children"),this),a&&(e?r&&r._cfg.tobeRemoved.push(a):a.parentNode.removeChild(a)),(t||void 0===t)&&this.destroy(),this},destroy:function(){this.get("destroyed")||(this._attrs=null,this.removeEvent(),this._cfg={destroyed:!0})},toFront:function(){var t=this._cfg,e=t.parent;if(e){var n=e._cfg.children,i=t.el,r=n.indexOf(this);n.splice(r,1),n.push(this),i&&(i.parentNode.removeChild(i),t.el=null)}},toBack:function(){var t=this._cfg,e=t.parent;if(e){var n=e._cfg.children,i=t.el,r=n.indexOf(this);if(n.splice(r,1),n.unshift(this),i){var a=i.parentNode;a.removeChild(i),a.insertBefore(i,a.firstChild)}}},_beforeSetZIndex:function(t){var e=this._cfg.parent;this._cfg.zIndex=t,i.isNil(e)||e.sort();var n=this._cfg.el;if(n){var r=e._cfg.children,a=r.indexOf(this),o=n.parentNode;o.removeChild(n),a===r.length-1?o.appendChild(n):o.insertBefore(n,o.childNodes[a])}return t},_setAttrs:function(t){return this.attr(t),t},setZIndex:function(t){return this._cfg.zIndex=t,this._beforeSetZIndex(t)},clone:function(){return i.clone(this)},getBBox:function(){}}),t.exports=l},function(t,e,n){function i(t,e,n,i){var r=1-i;return r*(r*t+2*i*e)+i*i*n}function r(t,e,n,r,a,s,l,u,c){var h,f,p,g,d,v,y,x=.005,m=1/0,_=[l,u];for(d=0;d<1;d+=.05)p=[i(t,n,a,d),i(e,r,s,d)],(f=o.squaredDistance(_,p))=0&&f=0?[r]:[]}}},function(t,e){t.exports={xAt:function(t,e,n,i,r){return e*Math.cos(t)*Math.cos(r)-n*Math.sin(t)*Math.sin(r)+i},yAt:function(t,e,n,i,r){return e*Math.sin(t)*Math.cos(r)+n*Math.cos(t)*Math.sin(r)+i},xExtrema:function(t,e,n){return Math.atan(-n/e*Math.tan(t))},yExtrema:function(t,e,n){return Math.atan(n/(e*Math.tan(t)))}}},function(t,e,n){function i(t,e,n){return t+e*Math.cos(n)}function r(t,e,n){return t+e*Math.sin(n)}var a=n(1),o=n(6),s=n(37),l=n(38),u=function t(e){t.superclass.constructor.call(this,e)};u.ATTRS={x:0,y:0,r:0,startAngle:0,endAngle:0,clockwise:!1,lineWidth:1,startArrow:!1,endArrow:!1},a.extend(u,o),a.augment(u,{canStroke:!0,type:"arc",getDefaultAttrs:function(){return{x:0,y:0,r:0,startAngle:0,endAngle:0,clockwise:!1,lineWidth:1,startArrow:!1,endArrow:!1}},calculateBox:function(){var t=this._attrs,e=t.x,n=t.y,i=t.r,r=t.startAngle,a=t.endAngle,o=t.clockwise,l=this.getHitLineWidth()/2,u=s.box(e,n,i,r,a,o);return u.minX-=l,u.minY-=l,u.maxX+=l,u.maxY+=l,u},getStartTangent:function(){var t=this._attrs,e=t.x,n=t.y,a=t.startAngle,o=t.r,s=t.clockwise,l=Math.PI/180;s&&(l*=-1);var u=[],c=i(e,o,a+l),h=r(n,o,a+l),f=i(e,o,a),p=r(n,o,a);return u.push([c,h]),u.push([f,p]),u},getEndTangent:function(){var t=this._attrs,e=t.x,n=t.y,a=t.endAngle,o=t.r,s=t.clockwise,l=Math.PI/180,u=[];s&&(l*=-1);var c=i(e,o,a+l),h=r(n,o,a+l),f=i(e,o,a),p=r(n,o,a);return u.push([f,p]),u.push([c,h]),u},createPath:function(t){var e=this._attrs,n=e.x,i=e.y,r=e.r,a=e.startAngle,o=e.endAngle,s=e.clockwise;(t=t||self.get("context")).beginPath(),t.arc(n,i,r,a,o,s)},afterPath:function(t){var e=this._attrs;if(t=t||this.get("context"),e.startArrow){var n=this.getStartTangent();l.addStartArrow(t,e,n[0][0],n[0][1],n[1][0],n[1][1])}if(e.endArrow){var i=this.getEndTangent();l.addEndArrow(t,e,i[0][0],i[0][1],i[1][0],i[1][1])}}}),t.exports=u},function(t,e,n){var i=n(1),r=n(6),a=function t(e){t.superclass.constructor.call(this,e)};a.ATTRS={x:0,y:0,r:0,lineWidth:1},i.extend(a,r),i.augment(a,{canFill:!0,canStroke:!0,type:"circle",getDefaultAttrs:function(){return{lineWidth:1}},calculateBox:function(){var t=this._attrs,e=t.x,n=t.y,i=t.r,r=this.getHitLineWidth()/2+i;return{minX:e-r,minY:n-r,maxX:e+r,maxY:n+r}},createPath:function(t){var e=this._attrs,n=e.x,i=e.y,r=e.r;t.beginPath(),t.arc(n,i,r,0,2*Math.PI,!1),t.closePath()}}),t.exports=a},function(t,e,n){var i=n(1),r=n(6),a=function t(e){t.superclass.constructor.call(this,e)};i.extend(a,r),i.augment(a,{canFill:!0,canStroke:!0,type:"dom",calculateBox:function(){var t=this._attrs,e=t.x,n=t.y,i=t.width,r=t.height,a=this.getHitLineWidth()/2;return{minX:e-a,minY:n-a,maxX:e+i+a,maxY:n+r+a}}}),t.exports=a},function(t,e,n){var i=n(1),r=n(6),a=function t(e){t.superclass.constructor.call(this,e)};a.ATTRS={x:0,y:0,rx:1,ry:1,lineWidth:1},i.extend(a,r),i.augment(a,{canFill:!0,canStroke:!0,type:"ellipse",getDefaultAttrs:function(){return{lineWidth:1}},calculateBox:function(){var t=this._attrs,e=t.x,n=t.y,i=t.rx,r=t.ry,a=this.getHitLineWidth(),o=i+a/2,s=r+a/2;return{minX:e-o,minY:n-s,maxX:e+o,maxY:n+s}},createPath:function(t){var e=this._attrs,n=e.x,r=e.y,a=e.rx,o=e.ry;t=t||self.get("context");var s=a>o?a:o,l=a>o?1:a/o,u=a>o?o/a:1,c=[1,0,0,0,1,0,0,0,1];i.mat3.scale(c,c,[l,u]),i.mat3.translate(c,c,[n,r]),t.beginPath(),t.save(),t.transform(c[0],c[1],c[3],c[4],c[6],c[7]),t.arc(0,0,s,0,2*Math.PI),t.restore(),t.closePath()}}),t.exports=a},function(t,e,n){var i=n(1),r=n(6),a=n(37),o=function t(e){t.superclass.constructor.call(this,e)};o.ATTRS={x:0,y:0,rs:0,re:0,startAngle:0,endAngle:0,clockwise:!1,lineWidth:1},i.extend(o,r),i.augment(o,{canFill:!0,canStroke:!0,type:"fan",getDefaultAttrs:function(){return{clockwise:!1,lineWidth:1,rs:0,re:0}},calculateBox:function(){var t=this._attrs,e=t.x,n=t.y,i=t.rs,r=t.re,o=t.startAngle,s=t.endAngle,l=t.clockwise,u=this.getHitLineWidth(),c=a.box(e,n,i,o,s,l),h=a.box(e,n,r,o,s,l),f=u/2;return{minX:Math.min(c.minX,h.minX)-f,minY:Math.min(c.minY,h.minY)-f,maxX:Math.max(c.maxX,h.maxX)+f,maxY:Math.max(c.maxY,h.maxY)+f}},createPath:function(t){var e=this._attrs,n=e.x,i=e.y,r=e.rs,a=e.re,o=e.startAngle,s=e.endAngle,l=e.clockwise,u={x:Math.cos(o)*r+n,y:Math.sin(o)*r+i},c={x:Math.cos(o)*a+n,y:Math.sin(o)*a+i},h={x:Math.cos(s)*r+n,y:Math.sin(s)*r+i};(t=t||self.get("context")).beginPath(),t.moveTo(u.x,u.y),t.lineTo(c.x,c.y),t.arc(n,i,a,o,s,l),t.lineTo(h.x,h.y),t.arc(n,i,r,s,o,!l),t.closePath()}}),t.exports=o},function(t,e,n){var i=n(1),r=n(6),a=function t(e){t.superclass.constructor.call(this,e)};a.ATTRS={x:0,y:0,img:void 0,width:0,height:0,sx:null,sy:null,swidth:null,sheight:null},i.extend(a,r),i.augment(a,{type:"image",isHitBox:function(){return!1},calculateBox:function(){var t=this._attrs;this._cfg.attrs&&this._cfg.attrs.img===t.img||this._setAttrImg();var e=t.x,n=t.y;return{minX:e,minY:n,maxX:e+t.width,maxY:n+t.height}},_beforeSetLoading:function(t){var e=this.get("canvas");return!1===t&&!0===this.get("toDraw")&&(this._cfg.loading=!1,e.draw()),t},_setAttrImg:function(){var t=this,e=t._attrs,n=e.img;if(!i.isString(n))return n instanceof Image?(e.width||t.attr("width",n.width),e.height||t.attr("height",n.height),n):n instanceof HTMLElement&&i.isString(n.nodeName)&&"CANVAS"===n.nodeName.toUpperCase()?(e.width||t.attr("width",Number(n.getAttribute("width"))),e.height||t.attr("height",Number(n.getAttribute("height"))),n):n instanceof ImageData?(e.width||t.attr("width",n.width),e.height||t.attr("height",n.height),n):null;var r=new Image;r.onload=function(){if(t.get("destroyed"))return!1;t.attr("imgSrc",n),t.attr("img",r);var e=t.get("callback");e&&e.call(t),t.set("loading",!1)},r.src=n,r.crossOrigin="Anonymous",t.set("loading",!0)},drawInner:function(t){this._cfg.hasUpdate&&this._setAttrImg(),this.get("loading")?this.set("toDraw",!0):(this._drawImage(t),this._cfg.hasUpdate=!1)},_drawImage:function(t){var e=this._attrs,n=e.x,r=e.y,a=e.img,o=e.width,s=e.height,l=e.sx,u=e.sy,c=e.swidth,h=e.sheight;this.set("toDraw",!1);var f=a;if(f instanceof ImageData&&((f=new Image).src=a),f instanceof Image||f instanceof HTMLElement&&i.isString(f.nodeName)&&"CANVAS"===f.nodeName.toUpperCase()){if(i.isNil(l)||i.isNil(u)||i.isNil(c)||i.isNil(h))return void t.drawImage(f,n,r,o,s);if(!(i.isNil(l)||i.isNil(u)||i.isNil(c)||i.isNil(h)))return void t.drawImage(f,l,u,c,h,n,r,o,s)}}}),t.exports=a},function(t,e,n){var i=n(1),r=n(6),a=n(38),o=n(36),s=function t(e){t.superclass.constructor.call(this,e)};s.ATTRS={x1:0,y1:0,x2:0,y2:0,lineWidth:1,startArrow:!1,endArrow:!1},i.extend(s,r),i.augment(s,{canStroke:!0,type:"line",getDefaultAttrs:function(){return{lineWidth:1,startArrow:!1,endArrow:!1}},calculateBox:function(){var t=this._attrs,e=t.x1,n=t.y1,i=t.x2,r=t.y2,a=this.getHitLineWidth();return o.box(e,n,i,r,a)},createPath:function(t){var e=this._attrs,n=e.x1,i=e.y1,r=e.x2,a=e.y2;(t=t||self.get("context")).beginPath(),t.moveTo(n,i),t.lineTo(r,a)},afterPath:function(t){var e=this._attrs,n=e.x1,i=e.y1,r=e.x2,o=e.y2;t=t||this.get("context"),e.startArrow&&a.addStartArrow(t,e,r,o,n,i),e.endArrow&&a.addEndArrow(t,e,n,i,r,o)},getPoint:function(t){var e=this._attrs;return{x:o.at(e.x1,e.x2,t),y:o.at(e.y1,e.y2,t)}}}),t.exports=s},function(t,e,n){var i=n(1),r=n(6),a=n(39),o=n(27),s=n(38),l=n(57),u=n(55),c=function t(e){t.superclass.constructor.call(this,e)};c.ATTRS={path:null,lineWidth:1,startArrow:!1,endArrow:!1},i.extend(c,r),i.augment(c,{canFill:!0,canStroke:!0,type:"path",getDefaultAttrs:function(){return{lineWidth:1,startArrow:!1,endArrow:!1}},_afterSetAttrPath:function(t){if(i.isNil(t))return this.setSilent("segments",null),void this.setSilent("box",void 0);var e,n=o.parsePath(t),r=[];if(i.isArray(n)&&0!==n.length&&("M"===n[0][0]||"m"===n[0][0])){for(var s=n.length,l=0;lr&&(r=i.maxX),i.minYo&&(o=i.maxY))}),n===1/0||a===1/0?{minX:0,minY:0,maxX:0,maxY:0}:{minX:n,minY:a,maxX:r,maxY:o}},_setTcache:function(){var t,e,n,r,a=0,o=0,s=[],l=this._cfg.curve;l&&(i.each(l,function(t,e){n=l[e+1],r=t.length,n&&(a+=u.len(t[r-2],t[r-1],n[1],n[2],n[3],n[4],n[5],n[6]))}),i.each(l,function(i,c){n=l[c+1],r=i.length,n&&((t=[])[0]=o/a,e=u.len(i[r-2],i[r-1],n[1],n[2],n[3],n[4],n[5],n[6]),o+=e,t[1]=o/a,s.push(t))}),this._cfg.tCache=s)},_calculateCurve:function(){var t=this._attrs.path;this._cfg.curve=l.pathTocurve(t)},getStartTangent:function(){var t,e,n,r,a=this.get("segments");if(a.length>1)if(t=a[0].endPoint,e=a[1].endPoint,n=a[1].startTangent,r=[],i.isFunction(n)){var o=n();r.push([t.x-o[0],t.y-o[1]]),r.push([t.x,t.y])}else r.push([e.x,e.y]),r.push([t.x,t.y]);return r},getEndTangent:function(){var t,e,n,r,a=this.get("segments"),o=a.length;if(o>1)if(t=a[o-2].endPoint,e=a[o-1].endPoint,n=a[o-1].endTangent,r=[],i.isFunction(n)){var s=n();r.push([e.x-s[0],e.y-s[1]]),r.push([e.x,e.y])}else r.push([t.x,t.y]),r.push([e.x,e.y]);return r},getPoint:function(t){var e,n,r=this._cfg.tCache;r||(this._calculateCurve(),this._setTcache(),r=this._cfg.tCache);var a=this._cfg.curve;if(!r)return a?{x:a[0][1],y:a[0][2]}:null;i.each(r,function(i,r){t>=i[0]&&t<=i[1]&&(e=(t-i[0])/(i[1]-i[0]),n=r)});var o=a[n];if(i.isNil(o)||i.isNil(n))return null;var s=o.length,l=a[n+1];return{x:u.at(o[s-2],l[1],l[3],l[5],1-e),y:u.at(o[s-1],l[2],l[4],l[6],1-e)}},createPath:function(t){var e=this.get("segments");if(i.isArray(e)){(t=t||this.get("context")).beginPath();for(var n=e.length,r=0;ra&&(a=e),io&&(o=i)});var s=e/2;return{minX:n-s,minY:r-s,maxX:a+s,maxY:o+s}},createPath:function(t){var e=this._attrs.points;e.length<2||((t=t||this.get("context")).beginPath(),i.each(e,function(e,n){0===n?t.moveTo(e[0],e[1]):t.lineTo(e[0],e[1])}),t.closePath())}}),t.exports=a},function(t,e,n){var i=n(1),r=n(6),a=n(38),o=n(36),s=function t(e){t.superclass.constructor.call(this,e)};s.ATTRS={points:null,lineWidth:1,startArrow:!1,endArrow:!1,tCache:null},i.extend(s,r),i.augment(s,{canStroke:!0,type:"polyline",tCache:null,getDefaultAttrs:function(){return{lineWidth:1,startArrow:!1,endArrow:!1}},calculateBox:function(){var t=this._attrs,e=this.getHitLineWidth(),n=t.points;if(!n||0===n.length)return null;var r=1/0,a=1/0,o=-1/0,s=-1/0;i.each(n,function(t){var e=t[0],n=t[1];eo&&(o=e),ns&&(s=n)});var l=e/2;return{minX:r-l,minY:a-l,maxX:o+l,maxY:s+l}},_setTcache:function(){var t,e,n=this._attrs.points,r=0,a=0,s=[];n&&0!==n.length&&(i.each(n,function(t,e){n[e+1]&&(r+=o.len(t[0],t[1],n[e+1][0],n[e+1][1]))}),r<=0||(i.each(n,function(i,l){n[l+1]&&((t=[])[0]=a/r,e=o.len(i[0],i[1],n[l+1][0],n[l+1][1]),a+=e,t[1]=a/r,s.push(t))}),this.tCache=s))},createPath:function(t){var e,n,i=this._attrs.points;if(!(i.length<2)){for((t=t||this.get("context")).beginPath(),t.moveTo(i[0][0],i[0][1]),n=1,e=i.length-1;n=i[0]&&t<=i[1]&&(e=(t-i[0])/(i[1]-i[0]),n=r)}),{x:o.at(r[n][0],r[n+1][0],e),y:o.at(r[n][1],r[n+1][1],e)}}}),t.exports=s},function(t,e,n){var i=n(1),r=n(27).parseRadius,a=n(6),o=function t(e){t.superclass.constructor.call(this,e)};o.ATTRS={x:0,y:0,width:0,height:0,radius:0,lineWidth:1},i.extend(o,a),i.augment(o,{canFill:!0,canStroke:!0,type:"rect",getDefaultAttrs:function(){return{lineWidth:1,radius:0}},calculateBox:function(){var t=this._attrs,e=t.x,n=t.y,i=t.width,r=t.height,a=this.getHitLineWidth()/2;return{minX:e-a,minY:n-a,maxX:e+i+a,maxY:n+r+a}},createPath:function(t){var e=this._attrs,n=e.x,i=e.y,a=e.width,o=e.height,s=e.radius;if((t=t||this.get("context")).beginPath(),0===s)t.rect(n,i,a,o);else{var l=r(s);t.moveTo(n+l.r1,i),t.lineTo(n+a-l.r2,i),0!==l.r2&&t.arc(n+a-l.r2,i+l.r2,l.r2,-Math.PI/2,0),t.lineTo(n+a,i+o-l.r3),0!==l.r3&&t.arc(n+a-l.r3,i+o-l.r3,l.r3,0,Math.PI/2),t.lineTo(n+l.r4,i+o),0!==l.r4&&t.arc(n+l.r4,i+o-l.r4,l.r4,Math.PI/2,Math.PI),t.lineTo(n,i+l.r1),0!==l.r1&&t.arc(n+l.r1,i+l.r1,l.r1,Math.PI,1.5*Math.PI),t.closePath()}}}),t.exports=o},function(t,e,n){var i=n(1),r=n(6),a=function t(e){t.superclass.constructor.call(this,e)};a.ATTRS={x:0,y:0,text:null,fontSize:12,fontFamily:"sans-serif",fontStyle:"normal",fontWeight:"normal",fontVariant:"normal",textAlign:"start",textBaseline:"bottom",lineHeight:null,textArr:null},i.extend(a,r),i.augment(a,{canFill:!0,canStroke:!0,type:"text",getDefaultAttrs:function(){return{lineWidth:1,lineCount:1,fontSize:12,fontFamily:"sans-serif",fontStyle:"normal",fontWeight:"normal",fontVariant:"normal",textAlign:"start",textBaseline:"bottom"}},initTransform:function(){var t=this._attrs.fontSize;t&&+t<12&&this.transform([["t",-1*this._attrs.x,-1*this._attrs.y],["s",+t/12,+t/12],["t",this._attrs.x,this._attrs.y]])},_assembleFont:function(){var t=this._attrs,e=t.fontSize,n=t.fontFamily,i=t.fontWeight,r=t.fontStyle,a=t.fontVariant;t.font=[r,a,i,e+"px",n].join(" ")},_setAttrText:function(){var t=this._attrs,e=t.text,n=null;if(i.isString(e)&&-1!==e.indexOf("\n")){var r=(n=e.split("\n")).length;t.lineCount=r}t.textArr=n},_getTextHeight:function(){var t=this._attrs,e=t.lineCount,n=1*t.fontSize;if(e>1){return n*e+this._getSpaceingY()*(e-1)}return n},isHitBox:function(){return!1},calculateBox:function(){var t=this._attrs,e=this._cfg;e.attrs&&!e.hasUpdate||(this._assembleFont(),this._setAttrText()),t.textArr||this._setAttrText();var n=t.x,i=t.y,r=this.measureText();if(!r)return{minX:n,minY:i,maxX:n,maxY:i};var a=this._getTextHeight(),o=t.textAlign,s=t.textBaseline,l=this.getHitLineWidth(),u={x:n,y:i-a};o&&("end"===o||"right"===o?u.x-=r:"center"===o&&(u.x-=r/2)),s&&("top"===s?u.y+=a:"middle"===s&&(u.y+=a/2)),this.set("startPoint",u);var c=l/2;return{minX:u.x-c,minY:u.y-c,maxX:u.x+r+c,maxY:u.y+a+c}},_getSpaceingY:function(){var t=this._attrs,e=t.lineHeight,n=1*t.fontSize;return e?e-n:.14*n},drawInner:function(t){var e=this._attrs,n=this._cfg;n.attrs&&!n.hasUpdate||(this._assembleFont(),this._setAttrText()),t.font=e.font;var r=e.text;if(r){var a=e.textArr,o=e.x,s=e.y;if(t.beginPath(),this.hasStroke()){var l=e.strokeOpacity;i.isNil(l)||1===l||(t.globalAlpha=l),a?this._drawTextArr(t,!1):t.strokeText(r,o,s),t.globalAlpha=1}if(this.hasFill()){var u=e.fillOpacity;i.isNil(u)||1===u||(t.globalAlpha=u),a?this._drawTextArr(t,!0):t.fillText(r,o,s)}n.hasUpdate=!1}},_drawTextArr:function(t,e){var n,r=this._attrs.textArr,a=this._attrs.textBaseline,o=1*this._attrs.fontSize,s=this._getSpaceingY(),l=this._attrs.x,u=this._attrs.y,c=this.getBBox(),h=c.maxY-c.minY;i.each(r,function(i,r){n=u+r*(s+o)-h+o,"middle"===a&&(n+=h-o-(h-o)/2),"top"===a&&(n+=h-o),e?t.fillText(i,l,n):t.strokeText(i,l,n)})},measureText:function(){var t,e=this._attrs,n=e.text,r=e.font,a=e.textArr,o=0;if(!i.isNil(n)){var s=document.createElement("canvas").getContext("2d");return s.save(),s.font=r,a?i.each(a,function(e){t=s.measureText(e).width,ol&&(s=e.slice(l,s),c[u]?c[u]+=s:c[++u]=s),(n=n[0])===(o=o[0])?c[u]?c[u]+=o:c[++u]=o:(c[++u]=null,h.push({i:u,x:Object(i.a)(n,o)})),l=a.lastIndex;return lo&&(n=t,o=s)}),n}}},function(t,e){t.exports=parseInt},function(t,e){t.exports=function(t,e){return t.hasOwnProperty(e)}},function(t,e,n){var i=n(2),r=n(11),a=Object.values?function(t){return Object.values(t)}:function(t){var e=[];return i(t,function(n,i){r(t)&&"prototype"===i||e.push(n)}),e};t.exports=a},function(t,e,n){var i=n(137);t.exports=function(t,e,n,r,a){if(a)return[["M",+t+ +a,e],["l",n-2*a,0],["a",a,a,0,0,1,a,a],["l",0,r-2*a],["a",a,a,0,0,1,-a,a],["l",2*a-n,0],["a",a,a,0,0,1,-a,-a],["l",0,2*a-r],["a",a,a,0,0,1,a,-a],["z"]];var o=[["M",t,e],["l",n,0],["l",0,r],["l",-n,0],["z"]];return o.parsePathArray=i,o}},function(t,e){var n=/,?([a-z]),?/gi;t.exports=function(t){return t.join(",").replace(n,"$1")}},function(t,e,n){var i=n(139),r=function(t,e,n,i){return[t,e,n,i,n,i]},a=function(t,e,n,i,r,a){return[1/3*t+2/3*n,1/3*e+2/3*i,1/3*r+2/3*n,1/3*a+2/3*i,r,a]};t.exports=function(t,e){var n=i(t),o=e&&i(e),s={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},l={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},u=[],c=[],h="",f="",p=void 0,g=function(t,e,n){var i=void 0,o=void 0;if(!t)return["C",e.x,e.y,e.x,e.y,e.x,e.y];switch(!(t[0]in{T:1,Q:1})&&(e.qx=e.qy=null),t[0]){case"M":e.X=t[1],e.Y=t[2];break;case"A":t=["C"].concat(function t(e,n,i,r,a,o,s,l,u,c){i===r&&(i+=1);var h=120*Math.PI/180,f=Math.PI/180*(+a||0),p=[],g=void 0,d=void 0,v=void 0,y=void 0,x=void 0,m=function(t,e,n){return{x:t*Math.cos(n)-e*Math.sin(n),y:t*Math.sin(n)+e*Math.cos(n)}};if(c)d=c[0],v=c[1],y=c[2],x=c[3];else{e=(g=m(e,n,-f)).x,n=g.y,l=(g=m(l,u,-f)).x,u=g.y,e===l&&n===u&&(l+=1,u+=1);var _=(e-l)/2,b=(n-u)/2,w=_*_/(i*i)+b*b/(r*r);w>1&&(i*=w=Math.sqrt(w),r*=w);var S=i*i,M=r*r,C=(o===s?-1:1)*Math.sqrt(Math.abs((S*M-S*b*b-M*_*_)/(S*b*b+M*_*_)));y=C*i*b/r+(e+l)/2,x=C*-r*_/i+(n+u)/2,d=Math.asin(((n-x)/r).toFixed(9)),v=Math.asin(((u-x)/r).toFixed(9)),d=ev&&(d-=2*Math.PI),!s&&v>d&&(v-=2*Math.PI)}var A=v-d;if(Math.abs(A)>h){var k=v,P=l,T=u;v=d+h*(s&&v>d?1:-1),p=t(l=y+i*Math.cos(v),u=x+r*Math.sin(v),i,r,a,0,s,P,T,[v,k,y,x])}A=v-d;var I=Math.cos(d),O=Math.sin(d),L=Math.cos(v),E=Math.sin(v),D=Math.tan(A/4),F=4/3*i*D,B=4/3*r*D,R=[e,n],j=[e+F*O,n-B*I],N=[l+F*E,u-B*L],z=[l,u];if(j[0]=2*R[0]-j[0],j[1]=2*R[1]-j[1],c)return[j,N,z].concat(p);for(var Y=[],V=0,X=(p=[j,N,z].concat(p).join().split(",")).length;V7){t[e].shift();for(var i=t[e];i.length;)u[e]="A",o&&(c[e]="A"),t.splice(e++,0,["C"].concat(i.splice(0,6)));t.splice(e,1),p=Math.max(n.length,o&&o.length||0)}},v=function(t,e,i,r,a){t&&e&&"M"===t[a][0]&&"M"!==e[a][0]&&(e.splice(a,0,["M",r.x,r.y]),i.bx=0,i.by=0,i.x=t[a][1],i.y=t[a][2],p=Math.max(n.length,o&&o.length||0))};p=Math.max(n.length,o&&o.length||0);for(var y=0;y180),0,l,e+n*Math.sin(-r*o)]]}else a=[["M",t,e],["m",0,-i],["a",n,i,0,1,1,0,2*i],["a",n,i,0,1,1,0,-2*i],["z"]];return a}var r=n(140),a=n(141);t.exports=function(t){if(!(t=r(t))||!t.length)return[["M",0,0]];var e=[],n=0,o=0,s=0,l=0,u=0,c=void 0,h=void 0;"M"===t[0][0]&&(s=n=+t[0][1],l=o=+t[0][2],u++,e[0]=["M",n,o]);for(var f,p,g=3===t.length&&"M"===t[0][0]&&"R"===t[1][0].toUpperCase()&&"Z"===t[2][0].toUpperCase(),d=u,v=t.length;d2&&(i.push([n].concat(o.splice(0,2))),s="l",n="m"===n?"l":"L"),"o"===s&&1===o.length&&i.push([n,o[0]]),"r"===s)i.push([n].concat(o));else for(;o.length>=e[s]&&(i.push([n].concat(o.splice(0,e[s]))),e[s]););}),i}},function(t,e){t.exports=function(t,e){for(var n=[],i=0,r=t.length;r-2*!e>i;i+=2){var a=[{x:+t[i-2],y:+t[i-1]},{x:+t[i],y:+t[i+1]},{x:+t[i+2],y:+t[i+3]},{x:+t[i+4],y:+t[i+5]}];e?i?r-4===i?a[3]={x:+t[0],y:+t[1]}:r-2===i&&(a[2]={x:+t[0],y:+t[1]},a[3]={x:+t[2],y:+t[3]}):a[0]={x:+t[r-2],y:+t[r-1]}:r-4===i?a[3]=a[2]:i||(a[0]={x:+t[i],y:+t[i+1]}),n.push(["C",(-a[0].x+6*a[1].x+a[2].x)/6,(-a[0].y+6*a[1].y+a[2].y)/6,(a[1].x+6*a[2].x-a[3].x)/6,(a[1].y+6*a[2].y-a[3].y)/6,a[2].x,a[2].y])}return n}},function(t,e,n){var i=n(23);t.exports=function(t){return i(t).toLowerCase()}},function(t,e,n){var i=n(23);t.exports=function(t){return i(t).toUpperCase()}},function(t,e,n){var i=n(145);t.exports=function(t,e){if(!e)return[t];var n=i(t,e),r=[];for(var a in n)r.push(n[a]);return r}},function(t,e,n){var i=n(11),r=n(4),a=n(146);t.exports=function(t,e){if(!e)return{0:t};if(!i(e)){var n=r(e)?e:e.replace(/\s+/g,"").split("*");e=function(t){for(var e="_",i=0,r=n.length;i');t.appendChild(a),this.set("wrapperEl",a),this.get("forceFit")&&(n=s.getWidth(t,n),this.set("width",n));var l=this.get("renderer"),u=new o({containerDOM:a,width:n,height:i,pixelRatio:"svg"===l?1:this.get("pixelRatio"),renderer:l});this.set("canvas",u)},n._initPlot=function(){this._initPlotBack();var t=this.get("canvas"),e=t.addGroup({zIndex:1}),n=t.addGroup({zIndex:0}),i=t.addGroup({zIndex:3});this.set("backPlot",e),this.set("middlePlot",n),this.set("frontPlot",i)},n._initPlotBack=function(){var t=this.get("canvas"),e=this.get("viewTheme"),n=t.addGroup(u,{padding:this.get("padding"),plotBackground:r.mix({},e.plotBackground,this.get("plotBackground")),background:r.mix({},e.background,this.get("background"))});this.set("plot",n),this.set("plotRange",n.get("plotRange"))},n._initEvents=function(){this.get("forceFit")&&window.addEventListener("resize",r.wrapBehavior(this,"_initForceFitEvent"))},n._initForceFitEvent=function(){var t=setTimeout(r.wrapBehavior(this,"forceFit"),200);clearTimeout(this.get("resizeTimer")),this.set("resizeTimer",t)},n._renderLegends=function(){var t=this.get("options").legends;if(r.isNil(t)||!1!==t){var e=this.get("legendController");if(e.options=t||{},e.plotRange=this.get("plotRange"),t&&t.custom)e.addCustomLegend();else{var n=this.getAllGeoms(),i=[];r.each(n,function(t){var n=t.get("view"),a=t.getAttrsForLegend();r.each(a,function(a){var o=a.type,s=a.getScale(o);if(s.field&&"identity"!==s.type&&!function(t,e){var n=!1;return r.each(t,function(t){var i=[].concat(t.values),r=[].concat(e.values);t.type!==e.type||t.field!==e.field||i.sort().toString()!==r.sort().toString()||(n=!0)}),n}(i,s)){i.push(s);var l=n.getFilteredOutValues(s.field);e.addLegend(s,a,t,l)}})});var a=this.getYScales();0===i.length&&a.length>1&&e.addMixedLegend(a,n)}e.alignLegends()}},n._renderTooltips=function(){var t=this.get("options");if(r.isNil(t.tooltip)||!1!==t.tooltip){var e=this.get("tooltipController");e.options=t.tooltip||{},e.renderTooltip()}},n.getAllGeoms=function(){var t=[];t=t.concat(this.get("geoms"));var e=this.get("views");return r.each(e,function(e){t=t.concat(e.get("geoms"))}),t},n.forceFit=function(){if(this&&!this.destroyed){var t=this.get("container"),e=this.get("width"),n=s.getWidth(t,e);if(0!==n&&n!==e){var i=this.get("height");this.changeSize(n,i)}return this}},n.resetPlot=function(){var t=this.get("plot"),e=this.get("padding");i(e,t.get("padding"))||(t.set("padding",e),t.repaint())},n.changeSize=function(t,e){this.get("canvas").changeSize(t,e);var n=this.get("plot");return this.set("width",t),this.set("height",e),n.repaint(),this.set("keepPadding",!0),this.repaint(),this.set("keepPadding",!1),this.emit("afterchangesize"),this},n.changeWidth=function(t){return this.changeSize(t,this.get("height"))},n.changeHeight=function(t){return this.changeSize(this.get("width"),t)},n.view=function(t){(t=t||{}).theme=this.get("theme"),t.parent=this,t.backPlot=this.get("backPlot"),t.middlePlot=this.get("middlePlot"),t.frontPlot=this.get("frontPlot"),t.canvas=this.get("canvas"),r.isNil(t.animate)&&(t.animate=this.get("animate")),t.options=r.mix({},this._getSharedOptions(),t.options);var e=new a(t);return e.set("_id","view"+this.get("views").length),this.get("views").push(e),this.emit("addview",{view:e}),e},n.removeView=function(t){var e=this.get("views");r.Array.remove(e,t),t.destroy()},n._getSharedOptions=function(){var t=this.get("options"),e={};return r.each(["scales","coord","axes"],function(n){e[n]=r.cloneDeep(t[n])}),e},n.getViewRegion=function(){var t=this.get("plotRange");return{start:t.bl,end:t.tr}},n.legend=function(t,e){var n=this.get("options");n.legends||(n.legends={});var i={};return!1===t?n.legends=!1:r.isObject(t)?i=t:r.isString(t)?i[t]=e:i=e,r.mix(n.legends,i),this},n.tooltip=function(t,e){var n=this.get("options");return n.tooltip||(n.tooltip={}),!1===t?n.tooltip=!1:r.isObject(t)?r.mix(n.tooltip,t):r.mix(n.tooltip,e),this},n.clear=function(){this.emit("beforeclear");for(var e=this.get("views");e.length>0;){e.shift().destroy()}t.prototype.clear.call(this);var n=this.get("canvas");return this.resetPlot(),n.draw(),this.emit("afterclear"),this},n.clearInner=function(){var e=this.get("views");r.each(e,function(t){t.clearInner()});var n=this.get("tooltipController");if(n&&n.clear(),!this.get("keepLegend")){var i=this.get("legendController");i&&i.clear()}t.prototype.clearInner.call(this)},n.drawComponents=function(){t.prototype.drawComponents.call(this),this.get("keepLegend")||this._renderLegends()},n.render=function(){if(!this.get("keepPadding")&&this._isAutoPadding()){this.beforeRender(),this.drawComponents();var e=this._getAutoPadding(),n=this.get("plot");i(n.get("padding"),e)||(n.set("padding",e),n.repaint())}var a=this.get("middlePlot");if(this.get("limitInPlot")&&!a.attr("clip")){var o=r.getClipByRange(this.get("plotRange"));a.attr("clip",o)}t.prototype.render.call(this),this._renderTooltips()},n.repaint=function(){this.get("keepPadding")||this.resetPlot(),t.prototype.repaint.call(this)},n.changeVisible=function(t){var e=this.get("wrapperEl"),n=t?"":"none";e.style.display=n},n.toDataURL=function(){var t=this.get("canvas"),e=this.get("renderer"),n=t.get("el"),i="";if("svg"===e){var r=n.cloneNode(!0),a=document.implementation.createDocumentType("svg","-//W3C//DTD SVG 1.1//EN","http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"),o=document.implementation.createDocument("http://www.w3.org/2000/svg","svg",a);o.replaceChild(r,o.documentElement);var s=(new XMLSerializer).serializeToString(o);i="data:image/svg+xml;charset=utf8,"+encodeURIComponent(s)}else"canvas"===e&&(i=n.toDataURL("image/png"));return i},n.downloadImage=function(t){var e=this,n=document.createElement("a"),i=e.get("renderer"),r=(t||"chart")+("svg"===i?".svg":".png");e.get("canvas").get("timeline").stopAllAnimations(),setTimeout(function(){var t=e.toDataURL();if(window.Blob&&window.URL&&"svg"!==i){for(var a=t.split(","),o=a[0].match(/:(.*?);/)[1],s=atob(a[1]),l=s.length,u=new Uint8Array(l);l--;)u[l]=s.charCodeAt(l);var c=new Blob([u],{type:o});window.navigator.msSaveBlob?window.navigator.msSaveBlob(c,r):n.addEventListener("click",function(){n.download=r,n.href=window.URL.createObjectURL(c)})}else n.addEventListener("click",function(){n.download=r,n.href=t});var h=document.createEvent("MouseEvents");h.initEvent("click",!1,!1),n.dispatchEvent(h)},16)},n.showTooltip=function(t){var e=this.getViewsByPoint(t);if(e.length){this.get("tooltipController").showTooltip(t,e)}return this},n.hideTooltip=function(){return this.get("tooltipController").hideTooltip(),this},n.getTooltipItems=function(t){var e=this.getViewsByPoint(t),n=[];return r.each(e,function(e){var i=e.get("geoms");r.each(i,function(e){var i=e.get("dataArray"),a=[];r.each(i,function(n){var i=e.findPoint(t,n);if(i){var r=e.getTipItems(i);a=a.concat(r)}}),n=n.concat(a)})}),n},n.destroy=function(){this.emit("beforedestroy"),clearTimeout(this.get("resizeTimer"));var e=this.get("canvas"),n=this.get("wrapperEl");n.parentNode.removeChild(n),t.prototype.destroy.call(this),e.destroy(),window.removeEventListener("resize",r.getWrapBehavior(this,"_initForceFitEvent")),this.emit("afterdestroy")},e}(a);t.exports=g},function(t,e,n){var i=n(53),r=n(0),a=function(t){function e(e){var n,i={visible:!0},a=(n=t.call(this)||this).getDefaultCfg();return n._attrs=i,r.assign(i,a,e),n}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){return{}},n.get=function(t){return this._attrs[t]},n.set=function(t,e){this._attrs[t]=e},n.show=function(){this.get("visible")||(this.set("visible",!0),this.changeVisible(!0))},n.hide=function(){this.get("visible")&&(this.set("visible",!1),this.changeVisible(!1))},n.changeVisible=function(){},n.destroy=function(){this._attrs={},this.removeAllListeners(),this.destroyed=!0},e}(i);t.exports=a},function(t,e,n){function i(t,e,n,i){return t[i]+(e[i]-t[i])*n}function r(t){return"#"+a(t[0])+a(t[1])+a(t[2])}function a(t){return t=Math.round(t),1===(t=t.toString(16)).length&&(t="0"+t),t}function o(t){var e=[];return e.push(parseInt(t.substr(1,2),16)),e.push(parseInt(t.substr(3,2),16)),e.push(parseInt(t.substr(5,2),16)),e}var s=n(9),l=n(10),u=n(2),c=/rgba?\(([\s.,0-9]+)\)/,h={},f=null,p={toRGB:function(t){if("#"===t[0]&&7===t.length)return t;f||(f=function(){var t=document.createElement("i");return t.title="Web Colour Picker",t.style.display="none",document.body.appendChild(t),t}());var e;if(h[t])e=h[t];else{f.style.color=t,e=document.defaultView.getComputedStyle(f,"").getPropertyValue("color");e=r(c.exec(e)[1].split(/\s*,\s*/)),h[t]=e}return e},rgb2arr:o,gradient:function(t){var e=[];return l(t)&&(t=t.split("-")),u(t,function(t){-1===t.indexOf("#")&&(t=p.toRGB(t)),e.push(o(t))}),function(t){return function(t,e){(isNaN(e)||!s(e)||e<0)&&(e=0),e>1&&(e=1);var n=t.length-1,a=Math.floor(n*e),o=n*e-a,l=t[a],u=a===n?l:t[a+1];return r([i(l,u,o,0),i(l,u,o,1),i(l,u,o,2)])}(e,t)}}};t.exports=p},function(t,e,n){var i=n(2),r={values:n(64)};t.exports={isAdjust:function(t){return this.adjustNames.indexOf(t)>=0},_getDimValues:function(t){var e={},n=[];if(this.xField&&this.isAdjust("x")&&n.push(this.xField),this.yField&&this.isAdjust("y")&&n.push(this.yField),i(n,function(n){var i=r.values(t,n);i.sort(function(t,e){return t-e}),e[n]=i}),!this.yField&&this.isAdjust("y")){var a=[0,1];e.y=a}return e},adjustData:function(t,e){var n=this,r=n._getDimValues(e);i(t,function(e,a){i(r,function(i,r){n.adjustDim(r,i,e,t.length,a)})})},getAdjustRange:function(t,e,n){var i,r,a=n.indexOf(e),o=n.length;return!this.yField&&this.isAdjust("y")?(i=0,r=1):o>1?(i=0===a?n[0]:n[a-1],r=a===o-1?n[o-1]:n[a+1],0!==a?i+=(e-i)/2:i-=(r-e)/2,a!==o-1?r-=(r-e)/2:r+=(e-n[o-2])/2):(i=0===e?0:e-.5,r=0===e?1:e+.5),{pre:i,next:r}},groupData:function(t,e){var n={};return i(t,function(t){var i=t[e];void 0===i&&(i=t[e]=0),n[i]||(n[i]=[]),n[i].push(t)}),n}}},function(t,e,n){var i={default:n(152),dark:n(304)};t.exports=i},function(t,e){var n,i,r='"-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto,"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",SimSun, "sans-serif"',a={defaultColor:"#1890FF",plotCfg:{padding:[20,20,95,80]},fontFamily:r,defaultLegendPosition:"bottom",colors:["#1890FF","#2FC25B","#FACC14","#223273","#8543E0","#13C2C2","#3436C7","#F04864"],colors_16:["#1890FF","#41D9C7","#2FC25B","#FACC14","#E6965C","#223273","#7564CC","#8543E0","#5C8EE6","#13C2C2","#5CA3E6","#3436C7","#B381E6","#F04864","#D598D9"],colors_24:["#1890FF","#66B5FF","#41D9C7","#2FC25B","#6EDB8F","#9AE65C","#FACC14","#E6965C","#57AD71","#223273","#738AE6","#7564CC","#8543E0","#A877ED","#5C8EE6","#13C2C2","#70E0E0","#5CA3E6","#3436C7","#8082FF","#DD81E6","#F04864","#FA7D92","#D598D9"],colors_pie:["#1890FF","#13C2C2","#2FC25B","#FACC14","#F04864","#8543E0","#3436C7","#223273"],colors_pie_16:["#1890FF","#73C9E6","#13C2C2","#6CD9B3","#2FC25B","#9DD96C","#FACC14","#E6965C","#F04864","#D66BCA","#8543E0","#8E77ED","#3436C7","#737EE6","#223273","#7EA2E6"],shapes:{point:["hollowCircle","hollowSquare","hollowDiamond","hollowBowtie","hollowTriangle","hollowHexagon","cross","tick","plus","hyphen","line"],line:["line","dash","dot"],area:["area"]},sizes:[1,10],opacities:[.1,.9],axis:{top:{position:"top",title:null,label:{offset:16,textStyle:{fill:"#545454",fontSize:12,lineHeight:16,textBaseline:"middle",fontFamily:r},autoRotate:!0},line:{lineWidth:1,stroke:"#BFBFBF"},tickLine:{lineWidth:1,stroke:"#BFBFBF",length:4,alignWithLabel:!0}},bottom:{position:"bottom",title:null,label:{offset:16,autoRotate:!0,textStyle:{fill:"#545454",fontSize:12,lineHeight:16,textBaseline:"middle",fontFamily:r}},line:{lineWidth:1,stroke:"#BFBFBF"},tickLine:{lineWidth:1,stroke:"#BFBFBF",length:4,alignWithLabel:!0}},left:{position:"left",title:null,label:{offset:8,autoRotate:!0,textStyle:{fill:"#545454",fontSize:12,lineHeight:16,textBaseline:"middle",fontFamily:r}},line:null,tickLine:null,grid:{zIndex:-1,lineStyle:{stroke:"#E9E9E9",lineWidth:1,lineDash:[3,3]},hideFirstLine:!0}},right:{position:"right",title:null,label:{offset:8,autoRotate:!0,textStyle:{fill:"#545454",fontSize:12,lineHeight:16,textBaseline:"middle",fontFamily:r}},line:null,tickLine:null,grid:{lineStyle:{stroke:"#E9E9E9",lineWidth:1,lineDash:[3,3]},hideFirstLine:!0}},circle:{zIndex:1,title:null,label:{offset:8,textStyle:{fill:"#545454",fontSize:12,lineHeight:16,fontFamily:r}},line:{lineWidth:1,stroke:"#BFBFBF"},tickLine:{lineWidth:1,stroke:"#BFBFBF",length:4,alignWithLabel:!0},grid:{lineStyle:{stroke:"#E9E9E9",lineWidth:1,lineDash:[3,3]},hideFirstLine:!0}},radius:{zIndex:0,label:{offset:12,textStyle:{fill:"#545454",fontSize:12,textBaseline:"middle",lineHeight:16,fontFamily:r}},line:{lineWidth:1,stroke:"#BFBFBF"},tickLine:{lineWidth:1,stroke:"#BFBFBF",length:4,alignWithLabel:!0},grid:{lineStyle:{stroke:"#E9E9E9",lineWidth:1,lineDash:[3,3]},type:"circle"}},helix:{grid:null,label:null,title:null,line:{lineWidth:1,stroke:"#BFBFBF"},tickLine:{lineWidth:1,length:4,stroke:"#BFBFBF",alignWithLabel:!0}}},label:{offset:20,textStyle:{fill:"#545454",fontSize:12,textBaseline:"middle",fontFamily:r}},treemapLabels:{offset:10,textStyle:{fill:"#fff",fontSize:12,textBaseline:"top",fontStyle:"bold",fontFamily:r}},innerLabels:{textStyle:{fill:"#fff",fontSize:12,textBaseline:"middle",fontFamily:r}},thetaLabels:{labelHeight:14,offset:30},legend:{right:{position:"right",layout:"vertical",itemMarginBottom:8,width:16,height:156,title:null,legendStyle:{LIST_CLASS:{textAlign:"left"}},textStyle:{fill:"#8C8C8C",fontSize:12,textAlign:"start",textBaseline:"middle",lineHeight:0,fontFamily:r},unCheckColor:"#bfbfbf"},left:{position:"left",layout:"vertical",itemMarginBottom:8,width:16,height:156,title:null,textStyle:{fill:"#8C8C8C",fontSize:12,textAlign:"start",textBaseline:"middle",lineHeight:20,fontFamily:r},unCheckColor:"#bfbfbf"},top:{position:"top",offset:[0,6],layout:"horizontal",title:null,itemGap:10,width:156,height:16,textStyle:{fill:"#8C8C8C",fontSize:12,textAlign:"start",textBaseline:"middle",lineHeight:20,fontFamily:r},unCheckColor:"#bfbfbf"},bottom:{position:"bottom",offset:[0,6],layout:"horizontal",title:null,itemGap:10,width:156,height:16,textStyle:{fill:"#8C8C8C",fontSize:12,textAlign:"start",textBaseline:"middle",lineHeight:20,fontFamily:r},unCheckColor:"#bfbfbf"},html:(n={},n["g2-legend"]={height:"auto",width:"auto",position:"absolute",overflow:"auto",fontSize:"12px",fontFamily:r,lineHeight:"20px",color:"#8C8C8C"},n["g2-legend-title"]={marginBottom:"4px"},n["g2-legend-list"]={listStyleType:"none",margin:0,padding:0},n["g2-legend-list-item"]={cursor:"pointer",marginBottom:"5px",marginRight:"24px"},n["g2-legend-marker"]={width:"9px",height:"9px",borderRadius:"50%",display:"inline-block",marginRight:"8px",verticalAlign:"middle"},n),gradient:{textStyle:{fill:"#8C8C8C",fontSize:12,textAlign:"center",textBaseline:"middle",lineHeight:20,fontFamily:r},lineStyle:{lineWidth:1,stroke:"#fff"},unCheckColor:"#bfbfbf"},margin:[0,5,24,5],legendMargin:24},tooltip:(i={useHtml:!0,crosshairs:!1,offset:15},i["g2-tooltip"]={position:"absolute",visibility:"hidden",zIndex:8,transition:"visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1), left 0.4s cubic-bezier(0.23, 1, 0.32, 1), top 0.4s cubic-bezier(0.23, 1, 0.32, 1)",backgroundColor:"rgba(255, 255, 255, 0.9)",boxShadow:"0px 0px 10px #aeaeae",borderRadius:"3px",color:"rgb(87, 87, 87)",fontSize:"12px",fontFamily:r,lineHeight:"20px",padding:"10px 10px 6px 10px"},i["g2-tooltip-title"]={marginBottom:"4px"},i["g2-tooltip-list"]={margin:0,listStyleType:"none",padding:0},i["g2-tooltip-list-item"]={marginBottom:"4px"},i["g2-tooltip-marker"]={width:"5px",height:"5px",borderRadius:"50%",display:"inline-block",marginRight:"8px"},i["g2-tooltip-value"]={display:"inline-block",float:"right",marginLeft:"30px"},i),tooltipMarker:{symbol:function(t,e,n){return[["M",t,e],["m",-n,0],["a",n,n,0,1,0,2*n,0],["a",n,n,0,1,0,2*-n,0]]},stroke:"#fff",shadowBlur:10,shadowOffsetX:0,shadowOffSetY:0,shadowColor:"rgba(0,0,0,0.09)",lineWidth:2,radius:4},tooltipCrosshairsRect:{type:"rect",rectStyle:{fill:"#CCD6EC",opacity:.3}},tooltipCrosshairsLine:{lineStyle:{stroke:"rgba(0, 0, 0, 0.25)",lineWidth:1}},shape:{point:{lineWidth:1,fill:"#1890FF",radius:4},hollowPoint:{fill:"#fff",lineWidth:1,stroke:"#1890FF",radius:3},interval:{lineWidth:0,fill:"#1890FF",fillOpacity:.85},hollowInterval:{fill:"#fff",stroke:"#1890FF",fillOpacity:0,lineWidth:2},area:{lineWidth:0,fill:"#1890FF",fillOpacity:.6},polygon:{lineWidth:0,fill:"#1890FF",fillOpacity:1},hollowPolygon:{fill:"#fff",stroke:"#1890FF",fillOpacity:0,lineWidth:2},hollowArea:{fill:"#fff",stroke:"#1890FF",fillOpacity:0,lineWidth:2},line:{stroke:"#1890FF",lineWidth:2,fill:null},edge:{stroke:"#1890FF",lineWidth:1,fill:null},schema:{stroke:"#1890FF",lineWidth:1,fill:null}},guide:{line:{lineStyle:{stroke:"rgba(0, 0, 0, .65)",lineDash:[2,2],lineWidth:1},text:{position:"start",autoRotate:!0,style:{fill:"rgba(0, 0, 0, .45)",fontSize:12,textAlign:"start",fontFamily:r,textBaseline:"bottom"}}},text:{style:{fill:"rgba(0,0,0,.5)",fontSize:12,textBaseline:"middle",textAlign:"start",fontFamily:r}},region:{style:{lineWidth:0,fill:"#000",fillOpacity:.04}},html:{alignX:"middle",alignY:"middle"},dataRegion:{style:{region:{lineWidth:0,fill:"#000000",opacity:.04},text:{textAlign:"center",textBaseline:"bottom",fontSize:12,fill:"rgba(0, 0, 0, .65)"}}},dataMarker:{top:!0,style:{point:{r:3,fill:"#FFFFFF",stroke:"#1890FF",lineWidth:2},line:{stroke:"#A3B1BF",lineWidth:1},text:{fill:"rgba(0, 0, 0, .65)",opacity:1,fontSize:12,textAlign:"start"}},display:{point:!0,line:!0,text:!0},lineLength:20,direction:"upward",autoAdjust:!0}},pixelRatio:null};t.exports=a},function(t,e,n){var i=n(25).Group,r=n(3),a=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){return{zIndex:1,type:"line",lineStyle:null,items:null,alternateColor:null,matrix:null,hideFirstLine:!1,hideLastLine:!1,hightLightZero:!1,zeroLineStyle:{stroke:"#595959",lineDash:[0,0]}}},n._renderUI=function(){t.prototype._renderUI.call(this),this._drawLines()},n._drawLines=function(){var t=this.get("lineStyle"),e=this.get("items");e&&e.length&&(this._precessItems(e),this._drawGridLines(e,t))},n._precessItems=function(t){var e,n=this;r.each(t,function(t,i){e&&n.get("alternateColor")&&n._drawAlternativeBg(t,e,i),e=t})},n._drawGridLines=function(t,e){var n,i,a,o,s=this,l=this.get("type"),u=t.length;"line"===l||"polygon"===l?r.each(t,function(t,c){s.get("hideFirstLine")&&0===c||s.get("hideLastLine")&&c===u-1||(o=t.points,i=[],"line"===l?(i.push(["M",o[0].x,o[0].y]),i.push(["L",o[o.length-1].x,o[o.length-1].y])):r.each(o,function(t,e){0===e?i.push(["M",t.x,t.y]):i.push(["L",t.x,t.y])}),a=s._drawZeroLine(l,c)?r.mix({},s.get("zeroLineStyle"),{path:i}):r.mix({},e,{path:i}),(n=s.addShape("path",{attrs:a})).name="axis-grid",n._id=t._id,n.set("coord",s.get("coord")),s.get("appendInfo")&&n.setSilent("appendInfo",s.get("appendInfo")))}):r.each(t,function(t,l){s.get("hideFirstLine")&&0===l||s.get("hideLastLine")&&l===u-1||(o=t.points,i=[],r.each(o,function(t,e){var n=t.radius;0===e?i.push(["M",t.x,t.y]):i.push(["A",n,n,0,0,t.flag,t.x,t.y])}),a=r.mix({},e,{path:i}),(n=s.addShape("path",{attrs:a})).name="axis-grid",n._id=t._id,n.set("coord",s.get("coord")),s.get("appendInfo")&&n.setSilent("appendInfo",s.get("appendInfo")))})},n._drawZeroLine=function(t,e){var n=this.get("tickValues");return!("line"!==t||!n||0!==n[e]||!this.get("hightLightZero"))},n._drawAlternativeBg=function(t,e,n){var i,a,o,s=this.get("alternateColor");r.isString(s)?a=s:r.isArray(s)&&(a=s[0],o=s[1]),n%2==0?o&&(i=this._getBackItem(e.points,t.points,o)):a&&(i=this._getBackItem(e.points,t.points,a));var l=this.addShape("Path",{attrs:i});l.name="axis-grid-rect",l._id=t._id&&t._id.replace("grid","grid-rect"),l.set("coord",this.get("coord")),this.get("appendInfo")&&l.setSilent("appendInfo",this.get("appendInfo"))},n._getBackItem=function(t,e,n){var i=[],a=this.get("type");if("line"===a)i.push(["M",t[0].x,t[0].y]),i.push(["L",t[t.length-1].x,t[t.length-1].y]),i.push(["L",e[e.length-1].x,e[e.length-1].y]),i.push(["L",e[0].x,e[0].y]),i.push(["Z"]);else if("polygon"===a){r.each(t,function(t,e){0===e?i.push(["M",t.x,t.y]):i.push(["L",t.x,t.y])});for(var o=e.length-1;o>=0;o--)i.push(["L",e[o].x,e[o].y]);i.push(["Z"])}else{var s=t[0].flag;r.each(t,function(t,e){var n=t.radius;0===e?i.push(["M",t.x,t.y]):i.push(["A",n,n,0,0,t.flag,t.x,t.y])});for(var l=e.length-1;l>=0;l--){var u=e[l],c=u.radius;l===e.length-1?i.push(["M",u.x,u.y]):i.push(["A",c,c,0,0,1===s?0:1,u.x,u.y])}}return{fill:n,path:i}},e}(i);t.exports=a},function(t,e,n){var i=n(3),r=i.DomUtil,a=n(32),o={scatter:n(307),map:n(308),treemap:n(309)},s=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{name:"label",type:"default",textStyle:null,formatter:null,items:null,useHtml:!1,containerTpl:'
    ',itemTpl:'
    {text}
    ',labelLine:!1,lineGroup:null,shapes:null,config:!0,capture:!0})},n.clear=function(){var e=this.get("group"),n=this.get("container");e&&!e.get("destroyed")&&e.clear(),n&&(n.innerHTML=""),t.prototype.clear.call(this)},n.destroy=function(){var t=this.get("group"),e=this.get("container");t.destroy||t.destroy(),e&&(e.innerHTML="")},n.render=function(){this.clear(),this._init(),this.beforeDraw(),this.draw(),this.afterDraw()},n._dryDraw=function(){var t=this,e=t.get("items"),n=t.getLabels(),r=n.length;i.each(e,function(e,i){if(i=e.length;a--)n[a].remove();t._adjustLabels(),!t.get("labelLine")&&t.get("config")||t.drawLines()},n.draw=function(){this._dryDraw(),this.get("canvas").draw()},n.changeLabel=function(t,e){if(t)if(t.tagName){var n=this._createDom(e);t.innerHTML=n.innerHTML,this._setCustomPosition(e,t)}else t._id=e._id,t.attr("text",e.text),t.attr("x")===e.x&&t.attr("y")===e.y||(t.resetMatrix(),e.textStyle.rotate&&(t.rotateAtStart(e.textStyle.rotate),delete e.textStyle.rotate),t.attr(e))},n.show=function(){var t=this.get("group"),e=this.get("container");t&&t.show(),e&&(e.style.opacity=1)},n.hide=function(){var t=this.get("group"),e=this.get("container");t&&t.hide(),e&&(e.style.opacity=0)},n.drawLines=function(){var t=this;"boolean"==typeof t.get("labelLine")&&t.set("labelLine",{});var e=t.get("lineGroup");!e||e.get("destroyed")?(e=t.get("group").addGroup({elCls:"x-line-group"}),t.set("lineGroup",e)):e.clear(),i.each(t.get("items"),function(n){t.lineToLabel(n,e)})},n.lineToLabel=function(t,e){if(this.get("config")||t.labelLine){var n=t.labelLine||this.get("labelLine"),r=void 0===t.capture?this.get("capture"):t.capture,a=n.path;if(a&&i.isFunction(n.path)&&(a=n.path(t)),!a){var o=t.start||{x:t.x-t._offset.x,y:t.y-t._offset.y};a=[["M",o.x,o.y],["L",t.x,t.y]]}var s=t.color;s||(s=t.textStyle&&t.textStyle.fill?t.textStyle.fill:"#000");var l=e.addShape("path",{attrs:i.mix({path:a,fill:null,stroke:s},n),capture:r});l.name=this.get("name"),l._id=t._id&&t._id.replace("glabel","glabelline"),l.set("coord",this.get("coord"))}},n._adjustLabels=function(){var t=this.get("type"),e=this.getLabels(),n=this.get("shapes"),i=o[t];"default"!==t&&i&&i(e,n)},n.getLabels=function(){var t=this.get("container");return t?i.toArray(t.childNodes):this.get("group").get("children")},n._addLabel=function(t,e){var n=t;return this.get("config")&&(n=this._getLabelCfg(t,e)),this._createText(n)},n._getLabelCfg=function(t,e){var n=this.get("textStyle")||{},r=this.get("formatter"),a=this.get("htmlTemplate");if(!i.isObject(t)){var o=t;(t={}).text=o}i.isFunction(n)&&(n=n(t.text,t,e)),r&&(t.text=r(t.text,t,e)),a&&(t.useHtml=!0,i.isFunction(a)&&(t.text=a(t.text,t,e))),i.isNil(t.text)&&(t.text=""),t.text=t.text+"";return i.mix({},t,{textStyle:n},{x:t.x||0,y:t.y||0})},n._init=function(){if(!this.get("group")){var t=this.get("canvas").addGroup({id:"label-group"});this.set("group",t)}},n.initHtmlContainer=function(){var t=this.get("container");if(t)i.isString(t)&&(t=document.getElementById(t))&&this.set("container",t);else{var e=this.get("containerTpl"),n=this.get("canvas").get("el").parentNode;t=r.createDom(e),n.style.position="relative",n.appendChild(t),this.set("container",t)}return t},n._createText=function(t){var e,n=this.get("container"),r=void 0===t.capture?this.get("capture"):t.capture;if(!t.useHtml&&!t.htmlTemplate){var a=this.get("name"),o=t.point,s=this.get("group");delete t.point;var l=t.rotate;return t.textStyle&&(t.textStyle.rotate&&(l=t.textStyle.rotate,delete t.textStyle.rotate),t=i.mix({x:t.x,y:t.y,textAlign:t.textAlign,text:t.text},t.textStyle)),e=s.addShape("text",{attrs:t,capture:r}),l&&(Math.abs(l)>2*Math.PI&&(l=l/180*Math.PI),e.transform([["t",-t.x,-t.y],["r",l],["t",t.x,t.y]])),e.setSilent("origin",o||t),e.name=a,this.get("appendInfo")&&e.setSilent("appendInfo",this.get("appendInfo")),e}n||(n=this.initHtmlContainer());var u=this._createDom(t);n.appendChild(u),this._setCustomPosition(t,u)},n._createDom=function(t){var e=this.get("itemTpl"),n=i.substitute(e,{text:t.text});return r.createDom(n)},n._setCustomPosition=function(t,e){var n=t.textAlign||"left",i=t.y,a=t.x,o=r.getOuterWidth(e);i-=r.getOuterHeight(e)/2,"center"===n?a-=o/2:"right"===n&&(a-=o),e.style.top=parseInt(i,10)+"px",e.style.left=parseInt(a,10)+"px"},e}(a);t.exports=s},function(t,e){var n=function(){function t(){this.bitmap=[]}var e=t.prototype;return e.hasGap=function(t){for(var e=!0,n=this.bitmap,i=Math.floor(t.minX),r=Math.ceil(t.maxX),a=Math.floor(t.minY),o=Math.ceil(t.maxY)-1,s=i;sn&&a.each(e,function(t){h=t.getBBox(),u=f||h.width,c=h.height+r,n-li&&a.each(n,function(t){p=t.getBBox(),h=p.width,f=p.height,u?g=u+r:h>g&&(g=h+r),i-c-1?t:t.parentNode?t.parentNode.className===h?t.parentNode:r(t.parentNode,e):null}function a(t,e){var n=null,i=e instanceof c?e.get("value"):e;return o.each(t,function(t){if(t.value===i)return n=t,!1}),n}var o=n(3),s=n(157),l=n(14).FONT_FAMILY,u=o.DomUtil,c=o.Group,h="g2-legend",f="g2-legend-list",p="g2-legend-list-item",g="g2-legend-marker",d=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return o.mix({},e,{type:"category-legend",container:null,containerTpl:'

      ',itemTpl:'
    • {value}
    • ',legendStyle:{},textStyle:{fill:"#333",fontSize:12,textAlign:"middle",textBaseline:"top",fontFamily:l},abridgeText:!1,tipTpl:'
      ',tipStyle:{display:"none",fontSize:"12px",backgroundColor:"#fff",position:"absolute",width:"auto",height:"auto",padding:"3px",boxShadow:"2px 2px 5px #888"},autoPosition:!0})},n._init=function(){},n.beforeRender=function(){},n.render=function(){this._renderHTML()},n._bindEvents=function(){var t=this,e=i(this.get("legendWrapper"),f);this.get("hoverable")&&(e.onmousemove=function(e){return t._onMousemove(e)},e.onmouseout=function(e){return t._onMouseleave(e)}),this.get("clickable")&&(e.onclick=function(e){return t._onClick(e)})},n._onMousemove=function(t){var e=this.get("items"),n=t.target,i=n.className;if(!((i=i.split(" ")).indexOf(h)>-1||i.indexOf(f)>-1)){var o=r(n,p),s=a(e,o.getAttribute("data-value"));s?(this.deactivate(),this.activate(o.getAttribute("data-value")),this.emit("itemhover",{item:s,currentTarget:o,checked:s.checked})):s||(this.deactivate(),this.emit("itemunhover",t))}},n._onMouseleave=function(t){this.deactivate(),this.emit("itemunhover",t)},n._onClick=function(t){var e=this,n=i(this.get("legendWrapper"),f),s=this.get("unCheckColor"),l=this.get("items"),u=this.get("selectedMode"),c=n.childNodes,d=t.target,v=d.className;if(!((v=v.split(" ")).indexOf(h)>-1||v.indexOf(f)>-1)){var y=r(d,p),x=i(y,"g2-legend-text"),m=i(y,g),_=a(l,y.getAttribute("data-value"));if(_){var b=y.className,w=y.getAttribute("data-color");if("single"===u)_.checked=!0,o.each(c,function(t){if(t!==y){i(t,g).style.backgroundColor=s,t.className=t.className.replace("checked","unChecked"),t.style.color=s;a(l,t.getAttribute("data-value")).checked=!1}else x&&(x.style.color=e.get("textStyle").fill),m&&(m.style.backgroundColor=w),y.className=b.replace("unChecked","checked")});else{var S=-1!==b.indexOf("checked"),M=0;if(o.each(c,function(t){-1!==t.className.indexOf("checked")&&M++}),!this.get("allowAllCanceled")&&S&&1===M)return void this.emit("clicklastitem",{item:_,currentTarget:y,checked:"single"===u||_.checked});_.checked=!_.checked,S?(m&&(m.style.backgroundColor=s),y.className=b.replace("checked","unChecked"),y.style.color=s):(m&&(m.style.backgroundColor=w),y.className=b.replace("unChecked","checked"),y.style.color=this.get("textStyle").fill)}this.emit("itemclick",{item:_,currentTarget:y,checked:"single"===u||_.checked})}}},n.activate=function(t){var e=this,n=this,r=n.get("items"),o=a(r,t);i(n.get("legendWrapper"),f).childNodes.forEach(function(t){var s=i(t,g),l=a(r,t.getAttribute("data-value"));if(e.get("highlight")){if(l===o&&l.checked)return void(s.style.border="1px solid #333")}else l===o?s.style.opacity=n.get("activeOpacity"):l.checked&&(s.style.opacity=n.get("inactiveOpacity"))})},n.deactivate=function(){var t=this,e=this;i(e.get("legendWrapper"),f).childNodes.forEach(function(n){var r=i(n,g);t.get("highlight")?r.style.border="1px solid #fff":r.style.opacity=e.get("inactiveOpacity")})},n._renderHTML=function(){var t=this,e=this.get("container"),n=this.get("title"),r=this.get("containerTpl"),a=u.createDom(r),s=i(a,"g2-legend-title"),c=i(a,f),d=this.get("unCheckColor"),v=o.deepMix({},{CONTAINER_CLASS:{height:"auto",width:"auto",position:"absolute",overflowY:"auto",fontSize:"12px",fontFamily:l,lineHeight:"20px",color:"#8C8C8C"},TITLE_CLASS:{marginBottom:this.get("titleGap")+"px",fontSize:"12px",color:"#333",textBaseline:"middle",fontFamily:l},LIST_CLASS:{listStyleType:"none",margin:0,padding:0,textAlign:"center"},LIST_ITEM_CLASS:{cursor:"pointer",marginBottom:"5px",marginRight:"24px"},MARKER_CLASS:{width:"9px",height:"9px",borderRadius:"50%",display:"inline-block",marginRight:"4px",verticalAlign:"middle"}},this.get("legendStyle"));if(/^\#/.test(e)||"string"==typeof e&&e.constructor===String){var y=e.replace("#","");(e=document.getElementById(y)).appendChild(a)}else{var x=this.get("position"),m={};m="left"===x||"right"===x?{maxHeight:(this.get("maxLength")||e.offsetHeight)+"px"}:{maxWidth:(this.get("maxLength")||e.offsetWidth)+"px"},u.modifyCSS(a,o.mix({},v.CONTAINER_CLASS,m,this.get(h))),e.appendChild(a)}u.modifyCSS(c,o.mix({},v.LIST_CLASS,this.get(f))),s&&(n&&n.text?(s.innerHTML=n.text,u.modifyCSS(s,o.mix({},v.TITLE_CLASS,this.get("g2-legend-title"),n))):a.removeChild(s));var _=this.get("items"),b=this.get("itemTpl"),w=this.get("position"),S=this.get("layout"),M="right"===w||"left"===w||"vertical"===S?"block":"inline-block",C=o.mix({},v.LIST_ITEM_CLASS,{display:M},this.get(p)),A=o.mix({},v.MARKER_CLASS,this.get(g));if(o.each(_,function(e,n){var r,s=e.checked,l=t._formatItemValue(e.value),h=e.marker.fill||e.marker.stroke,f=s?h:d;r=o.isFunction(b)?b(l,f,s,n):b;var p=o.substitute(r,o.mix({},e,{index:n,checked:s?"checked":"unChecked",value:l,color:f,originColor:h,originValue:e.value.replace(/\"/g,""")})),v=u.createDom(p);v.style.color=t.get("textStyle").fill;var y=i(v,g),x=i(v,"g2-legend-text");if(u.modifyCSS(v,C),y&&u.modifyCSS(y,A),s||(v.style.color=d,y&&(y.style.backgroundColor=d)),c.appendChild(v),t.get("abridgeText")){var m=l,_=v.offsetWidth,w=t.get("textStyle").fontSize;isNaN(w)&&(-1!==w.indexOf("pt")?w=1*parseFloat(w.substr(0,w.length-2))/72*96:-1!==w.indexOf("px")&&(w=parseFloat(w.substr(0,w.length-2))));var S=w*m.length,M=Math.floor(_/w);_<2*w?m="":_1&&(m=m.substr(0,M-1)+"..."),x.innerText=m,v.addEventListener("mouseover",function(){var t=i(a.parentNode,"textTip");t.style.display="block",t.style.left=v.offsetLeft+v.offsetWidth+"px",t.style.top=v.offsetTop+15+"px",t.innerText=l}),v.addEventListener("mouseout",function(){i(a.parentNode,"textTip").style.display="none"})}}),this.get("abridgeText")){var k=this.get("tipTpl"),P=u.createDom(k),T=this.get("tipStyle");u.modifyCSS(P,T),a.parentNode.appendChild(P),P.addEventListener("mouseover",function(){P.style.display="none"})}this.set("legendWrapper",a)},n._adjustPositionOffset=function(){var t=this.get("position"),e=this.get("offset"),n=this.get("offsetX"),i=this.get("offsetY");n&&(e[0]=n),i&&(e[1]=i);var r=this.get("legendWrapper");r.style.left=t[0]+"px",r.style.top=t[1]+"px",r.style.marginLeft=e[0]+"px",r.style.marginTop=e[1]+"px"},n.getWidth=function(){return u.getOuterWidth(this.get("legendWrapper"))},n.getHeight=function(){return u.getOuterHeight(this.get("legendWrapper"))},n.move=function(e,n){/^\#/.test(this.get("container"))?t.prototype.move.call(this,e,n):(u.modifyCSS(this.get("legendWrapper"),{left:e+"px",top:n+"px"}),this.set("x",e),this.set("y",n))},n.destroy=function(){var t=this.get("legendWrapper");t&&t.parentNode&&t.parentNode.removeChild(t)},e}(s);t.exports=d},function(t,e,n){var i=n(32),r=n(3),a=function(t){function e(e){var n;return(n=t.call(this,e)||this)._init_(),n.render(),n}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return r.mix({},e,{type:null,plot:null,plotRange:null,rectStyle:{fill:"#CCD6EC",opacity:.3},lineStyle:{stroke:"rgba(0, 0, 0, 0.25)",lineWidth:1},isTransposed:!1})},n._init_=function(){var t,e=this.get("plot");t="rect"===this.type?e.addGroup({zIndex:0}):e.addGroup(),this.set("container",t)},n._addLineShape=function(t,e){var n=this.get("container").addShape("line",{capture:!1,attrs:t});return this.set("crossLineShape"+e,n),n},n._renderHorizontalLine=function(t,e){var n=r.mix(this.get("lineStyle"),this.get("style")),i=r.mix({x1:e?e.bl.x:t.get("width"),y1:0,x2:e?e.br.x:0,y2:0},n);this._addLineShape(i,"X")},n._renderVerticalLine=function(t,e){var n=r.mix(this.get("lineStyle"),this.get("style")),i=r.mix({x1:0,y1:e?e.bl.y:t.get("height"),x2:0,y2:e?e.tl.y:0},n);this._addLineShape(i,"Y")},n._renderBackground=function(t,e){var n=r.mix(this.get("rectStyle"),this.get("style")),i=this.get("container"),a=r.mix({x:e?e.tl.x:0,y:e?e.tl.y:t.get("height"),width:e?e.br.x-e.bl.x:t.get("width"),height:e?Math.abs(e.tl.y-e.bl.y):t.get("height")},n),o=i.addShape("rect",{attrs:a,capture:!1});return this.set("crosshairsRectShape",o),o},n._updateRectShape=function(t){var e,n=this.get("crosshairsRectShape"),i=this.get("isTransposed"),a=t[0],o=t[t.length-1],s=i?"y":"x",l=i?"height":"width",u=a[s];if(t.length>1&&a[s]>o[s]&&(u=o[s]),this.get("width"))n.attr(s,u-this.get("crosshairs").width/2),n.attr(l,this.get("width"));else if(r.isArray(a.point[s])&&!a.size){var c=a.point[s][1]-a.point[s][0];n.attr(s,a.point[s][0]),n.attr(l,c)}else e=3*a.size/4,n.attr(s,u-e),1===t.length?n.attr(l,3*a.size/2):n.attr(l,Math.abs(o[s]-a[s])+2*e)},n.render=function(){var t=this.get("canvas"),e=this.get("plotRange"),n=this.get("isTransposed");switch(this.clear(),this.get("type")){case"x":this._renderHorizontalLine(t,e);break;case"y":this._renderVerticalLine(t,e);break;case"cross":this._renderHorizontalLine(t,e),this._renderVerticalLine(t,e);break;case"rect":this._renderBackground(t,e);break;default:n?this._renderHorizontalLine(t,e):this._renderVerticalLine(t,e)}},n.show=function(){var e=this.get("container");t.prototype.show.call(this),e.show()},n.hide=function(){var e=this.get("container");t.prototype.hide.call(this),e.hide()},n.clear=function(){var e=this.get("container");this.set("crossLineShapeX",null),this.set("crossLineShapeY",null),this.set("crosshairsRectShape",null),t.prototype.clear.call(this),e.clear()},n.destroy=function(){var e=this.get("container");t.prototype.destroy.call(this),e.remove()},n.setPosition=function(t,e,n){var i=this.get("crossLineShapeX"),r=this.get("crossLineShapeY"),a=this.get("crosshairsRectShape");r&&!r.get("destroyed")&&r.move(t,0),i&&!i.get("destroyed")&&i.move(0,e),a&&!a.get("destroyed")&&this._updateRectShape(n)},e}(i);t.exports=a},function(t,e){var n={_calcTooltipPosition:function(t,e,n,i,r,a){var o=0,s=0,l=20;if(a){var u=a.getBBox();o=u.width,s=u.height,t=u.x,e=u.y,l=5}switch(n){case"inside":t=t+o/2-i/2,e=e+s/2-r/2;break;case"top":t=t+o/2-i/2,e=e-r-l;break;case"left":t=t-i-l,e=e+s/2-r/2;break;case"right":t=t+o+l,e=e+s/2-r/2;break;case"bottom":default:t=t+o/2-i/2,e=e+s+l}return[t,e]},_constraintPositionInBoundary:function(t,e,n,i,r,a){return t+n+20>r?t=(t-=n+20)<0?0:t:t+20<0?t=20:t+=20,e+i+20>a?e=(e-=i+20)<0?0:e:e+20<0?e=20:e+=20,[t,e]},_constraintPositionInPlot:function(t,e,n,i,r,a){return t+n>r.tr.x&&(t-=n+40),tr.bl.y&&(e-=i+40),ee&&!a){t+=2*Math.asin(e/(2*o))}else o+=e;return{x:r.x+o*Math.cos(t),y:r.y+o*Math.sin(t),angle:t,r:o}},n.getArcPoint=function(t,e){var n;return e=e||0,n=a.isArray(t.x)||a.isArray(t.y)?{x:a.isArray(t.x)?t.x[e]:t.x,y:a.isArray(t.y)?t.y[e]:t.y}:t,this.transLabelPoint(n),n},n.getPointAngle=function(t){var e=this.get("coord");return r.getPointAngle(e,t)},n.getMiddlePoint=function(t){var e=this.get("coord"),n=t.length,i={x:0,y:0};return a.each(t,function(t){i.x+=t.x,i.y+=t.y}),i.x/=n,i.y/=n,i=e.convert(i)},n._isToMiddle=function(t){return t.x.length>2},n.getLabelPoint=function(t,e,n){var i,r=t.text[n],a=1;this._isToMiddle(e)?i=this.getMiddlePoint(e.points):(1===t.text.length&&0===n?n=1:0===n&&(a=-1),i=this.getArcPoint(e,n));var o=this.getDefaultOffset(t);o*=a;var s=this.getPointAngle(i),l=this.getCirclePoint(s,o,i);if(l?(l.text=r,l.angle=s,l.color=e.color):l={text:""},t.autoRotate||void 0===t.autoRotate){var u=l.textStyle?l.textStyle.rotate:null;u||(u=l.rotate||this.getLabelRotate(s,o,e)),l.rotate=u}return l.start={x:i.x,y:i.y},l},n._isEmitLabels=function(){return this.get("label").labelEmit},n.getLabelRotate=function(t){var e;return e=180*t/Math.PI,e+=90,this._isEmitLabels()&&(e-=90),e&&(e>90?e-=180:e<-90&&(e+=180)),e/180*Math.PI},n.getLabelAlign=function(t){var e,n=this.get("coord");if(this._isEmitLabels())e=t.angle<=Math.PI/2&&t.angle>-Math.PI/2?"left":"right";else if(n.isTransposed){var i=n.getCenter(),r=this.getDefaultOffset(t);e=Math.abs(t.x-i.x)<1?"center":t.angle>Math.PI||t.angle<=0?r>0?"left":"right":r>0?"right":"left"}else e="center";return e},e}(i);t.exports=o},function(t,e,n){t.exports={Scale:n(341),Coord:n(342),Axis:n(347),Guide:n(348),Legend:n(351),Tooltip:n(353),Event:n(354)}},function(t,e,n){function i(t,e,n){void 0===n&&(n=1);var i=[t.x,t.y,n];return a.vec3.transformMat3(i,i,e),{x:i[0],y:i[1]}}var r=n(16),a=n(0),o=n(167);t.exports=function(t,e){var n=e;return a.each(t.get("children"),function(t){if(t instanceof r.Group||t instanceof r.Path)n=o(n,t.getBBox());else if(t instanceof r.Text){var e=function(t){var e=t.getBBox(),n={x:e.minX,y:e.minY},r={x:e.maxX,y:e.maxY},a=t.attr("matrix");return n=i(n,a),r=i(r,a),{minX:n.x,minY:n.y,maxX:r.x,maxY:r.y}}(t),s=Math.abs(e.maxX-e.minX),l=Math.abs(e.maxY-e.minY);n=s0?e=0:n=0,n-e<5&&!l&&n-e>=1&&(l=1)),i(l)){var m=(n-e)/(v-1);l=a.snapFactorTo(m,x,"ceil"),f!==h&&((y=parseInt((n-e)/l,10))>f&&(y=f),ye&&(_-=l),n=a.fixedBase(M,l),e=a.fixedBase(_,l)}n=Math.min(n,d),e=Math.max(e,g),c.push(e);for(var C=1;Cn?(s=o,o=n):s>n&&(s=n),l1&&(e.minTickInterval=s-o),(a(e.min)||e._toTimeStamp(e.min)>o)&&(e.min=o),(a(e.max)||e._toTimeStamp(e.max)d&&(d=n);var m=d/x,_=i(p);if(m>.51){for(var b=Math.ceil(m),w=i(g),S=_;S<=w+b;S+=b)f.push(r(S));d=null}else if(m>.0834){for(var M=Math.ceil(m/.0834),C=a(p),A=function(t,e){var n=i(t),r=i(e),o=a(t);return 12*(r-n)+(a(e)-o)%12}(p,g),k=0;k<=A+M;k+=M)f.push(o(_,k+C));d=null}else if(d>.5*y){var P=new Date(p),T=P.getFullYear(),I=P.getMonth(p),O=P.getDate(),L=Math.ceil(d/y),E=function(t,e){return Math.ceil((e-t)/h)}(p,g);d=L*y;for(var D=0;Dc){var F=new Date(p),B=F.getFullYear(),R=F.getMonth(p),j=F.getDate(),N=F.getHours(),z=s.snapTo(u,Math.ceil(d/c)),Y=function(t,e){return Math.ceil((e-t)/c)}(p,g);d=z*c;for(var V=0;V<=Y+z;V+=z)f.push(new Date(B,R,j,N+V).getTime())}else if(d>6e4){var X=function(t,e){return Math.ceil((e-t)/6e4)}(p,g),H=Math.ceil(d/6e4);d=6e4*H;for(var W=0;W<=X+H;W+=H)f.push(p+6e4*W)}else{d<1e3&&(d=1e3),p=1e3*Math.floor(p/1e3);var G=Math.ceil((g-p)/1e3),q=Math.ceil(d/1e3);d=1e3*q;for(var U=0;U-1?r/(this.values.length-1):0,n+e*(i-n)},n.getText=function(t){var e="",n=this.translate(t);e=n>-1?this.values[n]:t;var i=this.formatter;return e=parseInt(e,10),e=i?i(e):a.format(e,this.mask)},n.getTicks=function(){var t=this,e=this.ticks,n=[];return l(e,function(e){var i;i=c(e)?e:{text:h(e)?e:t.getText(e),value:t.scale(e),tickValue:e},n.push(i)}),n},n._toTimeStamp=function(t){return s.toTimeStamp(t)},e}(r);i.TimeCat=f,t.exports=f},function(t,e,n){function i(t,e){return 1===t?1:Math.log(e)/Math.log(t)}var r=n(2),a=n(17),o=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n._initDefaultCfg=function(){t.prototype._initDefaultCfg.call(this),this.type="log",this.tickCount=10,this.base=2,this._minTick=null},n.calculateTicks=function(){var t,e=this.base;if(this.min<0)throw new Error("The minimum value must be greater than zero!");var n=i(e,this.max);if(this.min>0)t=Math.floor(i(e,this.min));else{var a=this.values,o=this.max;r(a,function(t){t>0&&t1&&(o=1),t=Math.floor(i(e,o)),this._minTick=t,this.positiveMin=o}for(var s=n-t,l=this.tickCount,u=Math.ceil(s/l),c=[],h=t;h=0?Math.floor(i(e,this.min)):0)>n){var r=n;n=t,t=r}for(var a=n-t,o=this.tickCount,s=Math.ceil(a/o),l=[],u=t;u0&&(r=1/Math.sqrt(r),t[0]=e[0]*r,t[1]=e[1]*r),t},e.dot=function(t,e){return t[0]*e[0]+t[1]*e[1]},e.cross=function(t,e,n){var i=e[0]*n[1]-e[1]*n[0];return t[0]=t[1]=0,t[2]=i,t},e.lerp=function(t,e,n,i){var r=e[0],a=e[1];return t[0]=r+i*(n[0]-r),t[1]=a+i*(n[1]-a),t},e.random=function(t,e){e=e||1;var n=2*h.RANDOM()*Math.PI;return t[0]=Math.cos(n)*e,t[1]=Math.sin(n)*e,t},e.transformMat2=function(t,e,n){var i=e[0],r=e[1];return t[0]=n[0]*i+n[2]*r,t[1]=n[1]*i+n[3]*r,t},e.transformMat2d=function(t,e,n){var i=e[0],r=e[1];return t[0]=n[0]*i+n[2]*r+n[4],t[1]=n[1]*i+n[3]*r+n[5],t},e.transformMat3=function(t,e,n){var i=e[0],r=e[1];return t[0]=n[0]*i+n[3]*r+n[6],t[1]=n[1]*i+n[4]*r+n[7],t},e.transformMat4=function(t,e,n){var i=e[0],r=e[1];return t[0]=n[0]*i+n[4]*r+n[12],t[1]=n[1]*i+n[5]*r+n[13],t},e.rotate=function(t,e,n,i){var r=e[0]-n[0],a=e[1]-n[1],o=Math.sin(i),s=Math.cos(i);return t[0]=r*s-a*o+n[0],t[1]=r*o+a*s+n[1],t},e.angle=function(t,e){var n=t[0],i=t[1],r=e[0],a=e[1],o=n*n+i*i;o>0&&(o=1/Math.sqrt(o));var s=r*r+a*a;s>0&&(s=1/Math.sqrt(s));var l=(n*r+i*a)*o*s;return l>1?0:l<-1?Math.PI:Math.acos(l)},e.str=function(t){return"vec2("+t[0]+", "+t[1]+")"},e.exactEquals=function(t,e){return t[0]===e[0]&&t[1]===e[1]},e.equals=function(t,e){var n=t[0],i=t[1],r=e[0],a=e[1];return Math.abs(n-r)<=h.EPSILON*Math.max(1,Math.abs(n),Math.abs(r))&&Math.abs(i-a)<=h.EPSILON*Math.max(1,Math.abs(i),Math.abs(a))};var h=function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e.default=t,e}(n(52));e.len=u,e.sub=r,e.mul=a,e.div=o,e.dist=s,e.sqrDist=l,e.sqrLen=c,e.forEach=function(){var t=i();return function(e,n,i,r,a,o){var s=void 0,l=void 0;for(n||(n=2),i||(i=0),l=r?Math.min(r*n+i,e.length):e.length,s=i;s0&&(a=1/Math.sqrt(a),t[0]=e[0]*a,t[1]=e[1]*a,t[2]=e[2]*a),t}function p(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}Object.defineProperty(e,"__esModule",{value:!0}),e.forEach=e.sqrLen=e.len=e.sqrDist=e.dist=e.div=e.mul=e.sub=void 0,e.create=i,e.clone=function(t){var e=new g.ARRAY_TYPE(3);return e[0]=t[0],e[1]=t[1],e[2]=t[2],e},e.length=r,e.fromValues=a,e.copy=function(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t},e.set=function(t,e,n,i){return t[0]=e,t[1]=n,t[2]=i,t},e.add=function(t,e,n){return t[0]=e[0]+n[0],t[1]=e[1]+n[1],t[2]=e[2]+n[2],t},e.subtract=o,e.multiply=s,e.divide=l,e.ceil=function(t,e){return t[0]=Math.ceil(e[0]),t[1]=Math.ceil(e[1]),t[2]=Math.ceil(e[2]),t},e.floor=function(t,e){return t[0]=Math.floor(e[0]),t[1]=Math.floor(e[1]),t[2]=Math.floor(e[2]),t},e.min=function(t,e,n){return t[0]=Math.min(e[0],n[0]),t[1]=Math.min(e[1],n[1]),t[2]=Math.min(e[2],n[2]),t},e.max=function(t,e,n){return t[0]=Math.max(e[0],n[0]),t[1]=Math.max(e[1],n[1]),t[2]=Math.max(e[2],n[2]),t},e.round=function(t,e){return t[0]=Math.round(e[0]),t[1]=Math.round(e[1]),t[2]=Math.round(e[2]),t},e.scale=function(t,e,n){return t[0]=e[0]*n,t[1]=e[1]*n,t[2]=e[2]*n,t},e.scaleAndAdd=function(t,e,n,i){return t[0]=e[0]+n[0]*i,t[1]=e[1]+n[1]*i,t[2]=e[2]+n[2]*i,t},e.distance=u,e.squaredDistance=c,e.squaredLength=h,e.negate=function(t,e){return t[0]=-e[0],t[1]=-e[1],t[2]=-e[2],t},e.inverse=function(t,e){return t[0]=1/e[0],t[1]=1/e[1],t[2]=1/e[2],t},e.normalize=f,e.dot=p,e.cross=function(t,e,n){var i=e[0],r=e[1],a=e[2],o=n[0],s=n[1],l=n[2];return t[0]=r*l-a*s,t[1]=a*o-i*l,t[2]=i*s-r*o,t},e.lerp=function(t,e,n,i){var r=e[0],a=e[1],o=e[2];return t[0]=r+i*(n[0]-r),t[1]=a+i*(n[1]-a),t[2]=o+i*(n[2]-o),t},e.hermite=function(t,e,n,i,r,a){var o=a*a,s=o*(2*a-3)+1,l=o*(a-2)+a,u=o*(a-1),c=o*(3-2*a);return t[0]=e[0]*s+n[0]*l+i[0]*u+r[0]*c,t[1]=e[1]*s+n[1]*l+i[1]*u+r[1]*c,t[2]=e[2]*s+n[2]*l+i[2]*u+r[2]*c,t},e.bezier=function(t,e,n,i,r,a){var o=1-a,s=o*o,l=a*a,u=s*o,c=3*a*s,h=3*l*o,f=l*a;return t[0]=e[0]*u+n[0]*c+i[0]*h+r[0]*f,t[1]=e[1]*u+n[1]*c+i[1]*h+r[1]*f,t[2]=e[2]*u+n[2]*c+i[2]*h+r[2]*f,t},e.random=function(t,e){e=e||1;var n=2*g.RANDOM()*Math.PI,i=2*g.RANDOM()-1,r=Math.sqrt(1-i*i)*e;return t[0]=Math.cos(n)*r,t[1]=Math.sin(n)*r,t[2]=i*e,t},e.transformMat4=function(t,e,n){var i=e[0],r=e[1],a=e[2],o=n[3]*i+n[7]*r+n[11]*a+n[15];return o=o||1,t[0]=(n[0]*i+n[4]*r+n[8]*a+n[12])/o,t[1]=(n[1]*i+n[5]*r+n[9]*a+n[13])/o,t[2]=(n[2]*i+n[6]*r+n[10]*a+n[14])/o,t},e.transformMat3=function(t,e,n){var i=e[0],r=e[1],a=e[2];return t[0]=i*n[0]+r*n[3]+a*n[6],t[1]=i*n[1]+r*n[4]+a*n[7],t[2]=i*n[2]+r*n[5]+a*n[8],t},e.transformQuat=function(t,e,n){var i=n[0],r=n[1],a=n[2],o=n[3],s=e[0],l=e[1],u=e[2],c=r*u-a*l,h=a*s-i*u,f=i*l-r*s,p=r*f-a*h,g=a*c-i*f,d=i*h-r*c,v=2*o;return c*=v,h*=v,f*=v,p*=2,g*=2,d*=2,t[0]=s+c+p,t[1]=l+h+g,t[2]=u+f+d,t},e.rotateX=function(t,e,n,i){var r=[],a=[];return r[0]=e[0]-n[0],r[1]=e[1]-n[1],r[2]=e[2]-n[2],a[0]=r[0],a[1]=r[1]*Math.cos(i)-r[2]*Math.sin(i),a[2]=r[1]*Math.sin(i)+r[2]*Math.cos(i),t[0]=a[0]+n[0],t[1]=a[1]+n[1],t[2]=a[2]+n[2],t},e.rotateY=function(t,e,n,i){var r=[],a=[];return r[0]=e[0]-n[0],r[1]=e[1]-n[1],r[2]=e[2]-n[2],a[0]=r[2]*Math.sin(i)+r[0]*Math.cos(i),a[1]=r[1],a[2]=r[2]*Math.cos(i)-r[0]*Math.sin(i),t[0]=a[0]+n[0],t[1]=a[1]+n[1],t[2]=a[2]+n[2],t},e.rotateZ=function(t,e,n,i){var r=[],a=[];return r[0]=e[0]-n[0],r[1]=e[1]-n[1],r[2]=e[2]-n[2],a[0]=r[0]*Math.cos(i)-r[1]*Math.sin(i),a[1]=r[0]*Math.sin(i)+r[1]*Math.cos(i),a[2]=r[2],t[0]=a[0]+n[0],t[1]=a[1]+n[1],t[2]=a[2]+n[2],t},e.angle=function(t,e){var n=a(t[0],t[1],t[2]),i=a(e[0],e[1],e[2]);f(n,n),f(i,i);var r=p(n,i);return r>1?0:r<-1?Math.PI:Math.acos(r)},e.str=function(t){return"vec3("+t[0]+", "+t[1]+", "+t[2]+")"},e.exactEquals=function(t,e){return t[0]===e[0]&&t[1]===e[1]&&t[2]===e[2]},e.equals=function(t,e){var n=t[0],i=t[1],r=t[2],a=e[0],o=e[1],s=e[2];return Math.abs(n-a)<=g.EPSILON*Math.max(1,Math.abs(n),Math.abs(a))&&Math.abs(i-o)<=g.EPSILON*Math.max(1,Math.abs(i),Math.abs(o))&&Math.abs(r-s)<=g.EPSILON*Math.max(1,Math.abs(r),Math.abs(s))};var g=function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e.default=t,e}(n(52));e.sub=o,e.mul=s,e.div=l,e.dist=u,e.sqrDist=c,e.len=r,e.sqrLen=h,e.forEach=function(){var t=i();return function(e,n,i,r,a,o){var s=void 0,l=void 0;for(n||(n=3),i||(i=0),l=r?Math.min(r*n+i,e.length):e.length,s=i;s2*Math.PI&&(t=t/180*Math.PI),this.transform([["t",-e,-n],["r",t],["t",e,n]])},move:function(t,e){var n=this.get("x")||0,i=this.get("y")||0;return this.translate(t-n,e-i),this.set("x",t),this.set("y",e),this},transform:function(t){var e=this,n=this._attrs.matrix;return o.each(t,function(t){switch(t[0]){case"t":e.translate(t[1],t[2]);break;case"s":e.scale(t[1],t[2]);break;case"r":e.rotate(t[1]);break;case"m":e.attr("matrix",o.mat3.multiply([],n,t[1])),e.clearTotalMatrix()}}),e},setTransform:function(t){return this.attr("matrix",[1,0,0,0,1,0,0,0,1]),this.transform(t)},getMatrix:function(){return this.attr("matrix")},setMatrix:function(t){return this.attr("matrix",t),this.clearTotalMatrix(),this},apply:function(t,e){var n;return n=e?this._getMatrixByRoot(e):this.attr("matrix"),o.vec3.transformMat3(t,t,n),this},_getMatrixByRoot:function(t){t=t||this;for(var e=this,n=[];e!==t;)n.unshift(e),e=e.get("parent");n.unshift(e);var i=[1,0,0,0,1,0,0,0,1];return o.each(n,function(t){o.mat3.multiply(i,t.attr("matrix"),i)}),i},getTotalMatrix:function(){var t=this._cfg.totalMatrix;if(!t){t=[1,0,0,0,1,0,0,0,1];var e=this._cfg.parent;if(e){a(t,e.getTotalMatrix())}a(t,this.attr("matrix")),this._cfg.totalMatrix=t}return t},clearTotalMatrix:function(){},invert:function(t){var e=this.getTotalMatrix();if(r(e))t[0]/=e[0],t[1]/=e[4];else{var n=o.mat3.invert([],e);n&&o.vec3.transformMat3(t,t,n)}return this},resetTransform:function(t){var e=this.attr("matrix");i(e)||t.transform(e[0],e[1],e[3],e[4],e[6],e[7])}}},function(t,e,n){var i=n(1),r={delay:"delay",rotate:"rotate"},a={fill:"fill",stroke:"stroke",fillStyle:"fillStyle",strokeStyle:"strokeStyle"};t.exports={animate:function(t,e,n,o,s){void 0===s&&(s=0);this.set("animating",!0);var l=this.get("timeline");l||(l=this.get("canvas").get("timeline"),this.setSilent("timeline",l));var u=this.get("animators")||[];l._timer||l.initTimer(),i.isNumber(o)&&(s=o,o=null),i.isFunction(n)?(o=n,n="easeLinear"):n=n||"easeLinear";var c=function(t,e){var n={matrix:null,attrs:{}},o=e._attrs;for(var s in t)if("transform"===s)n.matrix=i.transform(e.getMatrix(),t[s]);else if("rotate"===s)n.matrix=i.transform(e.getMatrix(),[["r",t[s]]]);else if("matrix"===s)n.matrix=t[s];else{if(a[s]&&/^[r,R,L,l]{1}[\s]*\(/.test(t[s]))continue;r[s]||o[s]===t[s]||(n.attrs[s]=t[s])}return n}(t,this),h={fromAttrs:function(t,e){var n={},i=e._attrs;for(var r in t.attrs)n[r]=i[r];return n}(c,this),toAttrs:c.attrs,fromMatrix:i.clone(this.getMatrix()),toMatrix:c.matrix,duration:e,easing:n,callback:o,delay:s,startTime:l.getTime(),id:i.uniqueId()};u.length>0?u=function(t,e){var n=e.delay,r=Object.prototype.hasOwnProperty;return i.each(e.toAttrs,function(e,a){i.each(t,function(t){n').getContext("2d"),l={arc:function(t,e){var n=this._attrs,i=n.x,r=n.y,o=n.r,s=n.startAngle,l=n.endAngle,u=n.clockwise,c=this.getHitLineWidth();return!!this.hasStroke()&&a.arcline(i,r,o,s,l,u,c,t,e)},circle:function(t,e){var n=this._attrs,i=n.x,r=n.y,o=n.r,s=this.getHitLineWidth(),l=this.hasFill(),u=this.hasStroke();return l&&u?a.circle(i,r,o,t,e)||a.arcline(i,r,o,0,2*Math.PI,!1,s,t,e):l?a.circle(i,r,o,t,e):!!u&&a.arcline(i,r,o,0,2*Math.PI,!1,s,t,e)},dom:function(t,e){if(!this._cfg.el)return!1;var n=this._cfg.el.getBBox();return a.box(n.x,n.x+n.width,n.y,n.y+n.height,t,e)},ellipse:function(t,e){var n=this._attrs,i=this.hasFill(),o=this.hasStroke(),s=n.x,l=n.y,u=n.rx,c=n.ry,h=this.getHitLineWidth(),f=u>c?u:c,p=u>c?1:u/c,g=u>c?c/u:1,d=[t,e,1],v=[1,0,0,0,1,0,0,0,1];r.mat3.scale(v,v,[p,g]),r.mat3.translate(v,v,[s,l]);var y=r.mat3.invert([],v);return r.vec3.transformMat3(d,d,y),i&&o?a.circle(0,0,f,d[0],d[1])||a.arcline(0,0,f,0,2*Math.PI,!1,h,d[0],d[1]):i?a.circle(0,0,f,d[0],d[1]):!!o&&a.arcline(0,0,f,0,2*Math.PI,!1,h,d[0],d[1])},fan:function(t,e){function n(){var t=o.arc.nearAngle(m,d,v,y);if(r.isNumberEqual(m,t)){var e=r.vec2.squaredLength(x);if(p*p<=e&&e<=g*g)return!0}return!1}function i(){var n=s.getHitLineWidth(),i={x:Math.cos(d)*p+h,y:Math.sin(d)*p+f},r={x:Math.cos(d)*g+h,y:Math.sin(d)*g+f},o={x:Math.cos(v)*p+h,y:Math.sin(v)*p+f},l={x:Math.cos(v)*g+h,y:Math.sin(v)*g+f};return!!(a.line(i.x,i.y,r.x,r.y,n,t,e)||a.line(o.x,o.y,l.x,l.y,n,t,e)||a.arcline(h,f,p,d,v,y,n,t,e)||a.arcline(h,f,g,d,v,y,n,t,e))}var s=this,l=s.hasFill(),u=s.hasStroke(),c=s._attrs,h=c.x,f=c.y,p=c.rs,g=c.re,d=c.startAngle,v=c.endAngle,y=c.clockwise,x=[t-h,e-f],m=r.vec2.angleTo([1,0],x);return l&&u?n()||i():l?n():!!u&&i()},image:function(t,e){var n=this._attrs;if(this.get("toDraw")||!n.img)return!1;this._cfg.attrs&&this._cfg.attrs.img===n.img||this._setAttrImg();var i=n.x,r=n.y,o=n.width,s=n.height;return a.rect(i,r,o,s,t,e)},line:function(t,e){var n=this._attrs,i=n.x1,r=n.y1,o=n.x2,s=n.y2,l=this.getHitLineWidth();return!!this.hasStroke()&&a.line(i,r,o,s,l,t,e)},path:function(t,e){function n(){if(!r.isEmpty(o)){for(var n=a.getHitLineWidth(),i=0,s=o.length;i=3&&o.push(n[0]),a.polyline(o,i,t,e)}var r=this,o=r.hasFill(),s=r.hasStroke();return o&&s?i(t,e,r)||n():o?i(t,e,r):!!s&&n()},polyline:function(t,e){var n=this._attrs;if(this.hasStroke()){var i=n.points;if(i.length<2)return!1;var r=n.lineWidth;return a.polyline(i,r,t,e)}return!1},rect:function(t,e){function n(){var n=r._attrs,i=n.x,o=n.y,s=n.width,l=n.height,u=n.radius,c=r.getHitLineWidth();if(0===u){var h=c/2;return a.line(i-h,o,i+s+h,o,c,t,e)||a.line(i+s,o-h,i+s,o+l+h,c,t,e)||a.line(i+s+h,o+l,i-h,o+l,c,t,e)||a.line(i,o+l+h,i,o-h,c,t,e)}return a.line(i+u,o,i+s-u,o,c,t,e)||a.line(i+s,o+u,i+s,o+l-u,c,t,e)||a.line(i+s-u,o+l,i+u,o+l,c,t,e)||a.line(i,o+l-u,i,o+u,c,t,e)||a.arcline(i+s-u,o+u,u,1.5*Math.PI,2*Math.PI,!1,c,t,e)||a.arcline(i+s-u,o+l-u,u,0,.5*Math.PI,!1,c,t,e)||a.arcline(i+u,o+l-u,u,.5*Math.PI,Math.PI,!1,c,t,e)||a.arcline(i+u,o+u,u,Math.PI,1.5*Math.PI,!1,c,t,e)}var r=this,o=r.hasFill(),s=r.hasStroke();return o&&s?i(t,e,r)||n():o?i(t,e,r):!!s&&n()},text:function(t,e){var n=this.getBBox();if(this.hasFill()||this.hasStroke())return a.box(n.minX,n.maxX,n.minY,n.maxY,t,e)}};t.exports={isPointInPath:function(t,e){var n=l[this.type];return!!n&&n.call(this,t,e)}}},function(t,e,n){function i(t,e,n){var i=e.startTime;if(ng.length?(p=a.parsePathString(o[f]),g=a.parsePathString(s[f]),g=a.fillPathByDiff(g,p),g=a.formatPath(g,p),e.fromAttrs.path=g,e.toAttrs.path=p):e.pathFormatted||(p=a.parsePathString(o[f]),g=a.parsePathString(s[f]),g=a.formatPath(g,p),e.fromAttrs.path=g,e.toAttrs.path=p,e.pathFormatted=!0),i[f]=[];for(var d=0;d0){for(var l=r._animators.length-1;l>=0;l--)if((t=r._animators[l]).get("destroyed"))a.removeAnimator(l);else{if(!t.get("pause").isPaused)for(var u=(e=t.get("animators")).length-1;u>=0;u--)n=e[u],(s=i(t,n,o))&&(e.splice(u,1),s=!1,n.callback&&n.callback());0===e.length&&a.removeAnimator(l)}r.canvas.draw()}})},addAnimator:function(t){this._animators.push(t)},removeAnimator:function(t){this._animators.splice(t,1)},isAnimating:function(){return!!this._animators.length},stop:function(){this._timer&&this._timer.stop()},stopAllAnimations:function(){this._animators.forEach(function(t){t.stopAnimate()}),this._animators=[],this.canvas.draw()},getTime:function(){return this._current}}),t.exports=h},function(t,e,n){"use strict";var i=n(58);e.a=function(t,e,n){var r=new i.a;return e=null==e?0:+e,r.restart(function(n){r.stop(),t(n+e)},e,n),r}},function(t,e,n){"use strict";var i=n(58);e.a=function(t,e,n){var r=new i.a,a=e;return null==e?(r.restart(t,e,n),r):(e=+e,n=null==n?Object(i.b)():+n,r.restart(function i(o){o+=a,r.restart(i,a+=e,n),t(o)},e,n),r)}},function(t,e,n){"use strict";e.a=function(t){return+t}},function(t,e,n){"use strict";e.a=function(t){return t*t},e.c=function(t){return t*(2-t)},e.b=function(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}},function(t,e,n){"use strict";e.a=function(t){return t*t*t},e.c=function(t){return--t*t*t+1},e.b=function(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}},function(t,e,n){"use strict";n.d(e,"a",function(){return i}),n.d(e,"c",function(){return r}),n.d(e,"b",function(){return a});var i=function t(e){function n(t){return Math.pow(t,e)}return e=+e,n.exponent=t,n}(3),r=function t(e){function n(t){return 1-Math.pow(1-t,e)}return e=+e,n.exponent=t,n}(3),a=function t(e){function n(t){return((t*=2)<=1?Math.pow(t,e):2-Math.pow(2-t,e))/2}return e=+e,n.exponent=t,n}(3)},function(t,e,n){"use strict";e.a=function(t){return 1-Math.cos(t*r)},e.c=function(t){return Math.sin(t*r)},e.b=function(t){return(1-Math.cos(i*t))/2};var i=Math.PI,r=i/2},function(t,e,n){"use strict";e.a=function(t){return Math.pow(2,10*t-10)},e.c=function(t){return 1-Math.pow(2,-10*t)},e.b=function(t){return((t*=2)<=1?Math.pow(2,10*t-10):2-Math.pow(2,10-10*t))/2}},function(t,e,n){"use strict";e.a=function(t){return 1-Math.sqrt(1-t*t)},e.c=function(t){return Math.sqrt(1- --t*t)},e.b=function(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}},function(t,e,n){"use strict";function i(t){return(t=+t)b?Math.pow(t,1/3):t/_+x}function s(t){return t>m?t*t*t:_*(t-x)}function l(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function u(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function c(t,e,n,r){return 1===arguments.length?function(t){if(t instanceof h)return new h(t.h,t.c,t.l,t.opacity);t instanceof a||(t=i(t));var e=Math.atan2(t.b,t.a)*g.b;return new h(e<0?e+360:e,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}(t):new h(t,e,n,null==r?1:r)}function h(t,e,n,i){this.h=+t,this.c=+e,this.l=+n,this.opacity=+i}e.a=r,e.b=c;var f=n(61),p=n(60),g=n(118),d=.95047,v=1,y=1.08883,x=4/29,m=6/29,_=3*m*m,b=m*m*m;Object(f.a)(a,r,Object(f.b)(p.a,{brighter:function(t){return new a(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new a(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,e=isNaN(this.a)?t:t+this.a/500,n=isNaN(this.b)?t:t-this.b/200;return t=v*s(t),e=d*s(e),n=y*s(n),new p.b(l(3.2404542*e-1.5371385*t-.4985314*n),l(-.969266*e+1.8760108*t+.041556*n),l(.0556434*e-.2040259*t+1.0572252*n),this.opacity)}})),Object(f.a)(h,c,Object(f.b)(p.a,{brighter:function(t){return new h(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new h(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return i(this).rgb()}}))},function(t,e,n){"use strict";function i(t,e,n,i){return 1===arguments.length?function(t){if(t instanceof r)return new r(t.h,t.s,t.l,t.opacity);t instanceof o.b||(t=Object(o.h)(t));var e=t.r/255,n=t.g/255,i=t.b/255,a=(d*i+p*e-g*n)/(d+p-g),l=i-a,u=(f*(n-a)-c*l)/h,v=Math.sqrt(u*u+l*l)/(f*a*(1-a)),y=v?Math.atan2(u,l)*s.b-120:NaN;return new r(y<0?y+360:y,v,a,t.opacity)}(t):new r(t,e,n,null==i?1:i)}function r(t,e,n,i){this.h=+t,this.s=+e,this.l=+n,this.opacity=+i}e.a=i;var a=n(61),o=n(60),s=n(118),l=-.14861,u=1.78277,c=-.29227,h=-.90649,f=1.97294,p=f*h,g=f*u,d=u*c-h*l;Object(a.a)(r,i,Object(a.b)(o.a,{brighter:function(t){return t=null==t?o.c:Math.pow(o.c,t),new r(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?o.d:Math.pow(o.d,t),new r(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*s.a,e=+this.l,n=isNaN(this.s)?0:this.s*e*(1-e),i=Math.cos(t),r=Math.sin(t);return new o.b(255*(e+n*(l*i+u*r)),255*(e+n*(c*i+h*r)),255*(e+n*(f*i)),this.opacity)}}))},function(t,e,n){"use strict";e.a=function(t,e){return t=+t,e-=t,function(n){return Math.round(t+e*n)}}},function(t,e,n){"use strict";function i(t,e,n,i){function a(t){return t.length?t.pop()+" ":""}return function(o,s){var l=[],u=[];return o=t(o),s=t(s),function(t,i,a,o,s,l){if(t!==a||i!==o){var u=s.push("translate(",null,e,null,n);l.push({i:u-4,x:Object(r.a)(t,a)},{i:u-2,x:Object(r.a)(i,o)})}else(a||o)&&s.push("translate("+a+e+o+n)}(o.translateX,o.translateY,s.translateX,s.translateY,l,u),function(t,e,n,o){t!==e?(t-e>180?e+=360:e-t>180&&(t+=360),o.push({i:n.push(a(n)+"rotate(",null,i)-2,x:Object(r.a)(t,e)})):e&&n.push(a(n)+"rotate("+e+i)}(o.rotate,s.rotate,l,u),function(t,e,n,o){t!==e?o.push({i:n.push(a(n)+"skewX(",null,i)-2,x:Object(r.a)(t,e)}):e&&n.push(a(n)+"skewX("+e+i)}(o.skewX,s.skewX,l,u),function(t,e,n,i,o,s){if(t!==n||e!==i){var l=o.push(a(o)+"scale(",null,",",null,")");s.push({i:l-4,x:Object(r.a)(t,n)},{i:l-2,x:Object(r.a)(e,i)})}else 1===n&&1===i||o.push(a(o)+"scale("+n+","+i+")")}(o.scaleX,o.scaleY,s.scaleX,s.scaleY,l,u),o=s=null,function(t){for(var e,n=-1,i=u.length;++n');return t.appendChild(n),this.type="canvas",this.canvas=n,this.context=n.getContext("2d"),this.toDraw=!1,this}var e=t.prototype;return e.beforeDraw=function(){var t=this.canvas;this.context&&this.context.clearRect(0,0,t.width,t.height)},e.draw=function(t){function e(){n.animateHandler=i.requestAnimationFrame(function(){n.animateHandler=void 0,n.toDraw&&e()}),n.beforeDraw();try{n._drawGroup(t)}catch(t){console.warn("error in draw canvas, detail as:"),console.warn(t),n.toDraw=!1}n.toDraw=!1}var n=this;n.animateHandler?n.toDraw=!0:e()},e.drawSync=function(t){this.beforeDraw(),this._drawGroup(t)},e._drawGroup=function(t){if(!t._cfg.removed&&!t._cfg.destroyed&&t._cfg.visible){var e=t._cfg.children,n=null;this.setContext(t);for(var i=0;i-1){var s=n[o];"fillStyle"===o&&(s=r.parseStyle(s,t,e)),"strokeStyle"===o&&(s=r.parseStyle(s,t,e)),"lineDash"===o&&e.setLineDash?i.isArray(s)?e.setLineDash(s):i.isString(s)&&e.setLineDash(s.split(" ")):e[o]=s}},t}();t.exports=o},function(t,e,n){function i(t,e){var n=t.match(c);r.each(n,function(t){t=t.split(":"),e.addColorStop(t[0],t[1])})}var r=n(1),a=/[MLHVQTCSAZ]([^MLHVQTCSAZ]*)/gi,o=/[^\s\,]+/gi,s=/^l\s*\(\s*([\d.]+)\s*\)\s*(.*)/i,l=/^r\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)\s*(.*)/i,u=/^p\s*\(\s*([axyn])\s*\)\s*(.*)/i,c=/[\d.]+:(#[^\s]+|[^\)]+\))/gi;t.exports={parsePath:function(t){return t=t||[],r.isArray(t)?t:r.isString(t)?(t=t.match(a),r.each(t,function(e,n){if((e=e.match(o))[0].length>1){var i=e[0].charAt(0);e.splice(1,0,e[0].substr(1)),e[0]=i}r.each(e,function(t,n){isNaN(t)||(e[n]=+t)}),t[n]=e}),t):void 0},parseStyle:function(t,e,n){if(r.isString(t)){if("("===t[1]||"("===t[2]){if("l"===t[0])return function(t,e,n){var a,o,l=s.exec(t),u=r.mod(r.toRadian(parseFloat(l[1])),2*Math.PI),c=l[2],h=e.getBBox();u>=0&&u<.5*Math.PI?(a={x:h.minX,y:h.minY},o={x:h.maxX,y:h.maxY}):.5*Math.PI<=u&&u');return t.appendChild(n),this.type="svg",this.canvas=n,this.context=new o(n),this.toDraw=!1,this}var e=t.prototype;return e.draw=function(t){function e(){n.animateHandler=i.requestAnimationFrame(function(){n.animateHandler=void 0,n.toDraw&&e()});try{n._drawChildren(t)}catch(t){console.warn("error in draw canvas, detail as:"),console.warn(t),n.toDraw=!1}n.toDraw=!1}var n=this;n.animateHandler?n.toDraw=!0:e()},e.drawSync=function(t){this._drawChildren(t)},e._drawGroup=function(t,e){var n=t._cfg;n.removed||n.destroyed||(n.tobeRemoved&&(i.each(n.tobeRemoved,function(t){t.parentNode&&t.parentNode.removeChild(t)}),n.tobeRemoved=[]),this._drawShape(t,e),n.children&&n.children.length>0&&this._drawChildren(t))},e._drawChildren=function(t){var e,n=t._cfg.children;if(n)for(var i=0;is?1:0,f=Math.abs(l-s)>Math.PI?1:0,p=n.rs,g=n.re,d=e(s,n.rs,a),v=e(l,n.rs,a);n.rs>0?(o.push("M "+c.x+","+c.y),o.push("L "+v.x+","+v.y),o.push("A "+p+","+p+",0,"+f+","+(1===h?0:1)+","+d.x+","+d.y),o.push("L "+u.x+" "+u.y)):(o.push("M "+a.x+","+a.y),o.push("L "+u.x+","+u.y)),o.push("A "+g+","+g+",0,"+f+","+h+","+c.x+","+c.y),n.rs>0?o.push("L "+v.x+","+v.y):o.push("Z"),r.el.setAttribute("d",o.join(" "))},e._updateText=function(t){var e=t._attrs,n=t._cfg.attrs,i=t._cfg.el;this._setFont(t);for(var r in e)if(e[r]!==n[r]){if("text"===r){this._setText(t,""+e[r]);continue}if("fillStyle"===r||"strokeStyle"===r){this._setColor(t,r,e[r]);continue}if("matrix"===r){this._setTransform(t);continue}l[r]&&i.setAttribute(l[r],e[r])}t._cfg.attrs=Object.assign({},t._attrs),t._cfg.hasUpdate=!1},e._setFont=function(t){var e=t.get("el"),n=t._attrs,i=n.fontSize;e.setAttribute("alignment-baseline",u[n.textBaseline]||"baseline"),e.setAttribute("text-anchor",c[n.textAlign]||"left"),i&&+i<12&&(n.matrix=[1,0,0,0,1,0,0,0,1],t.transform([["t",-n.x,-n.y],["s",+i/12,+i/12],["t",n.x,n.y]]))},e._setText=function(t,e){var n=t._cfg.el,r=t._attrs.textBaseline||"bottom";if(e)if(~e.indexOf("\n")){var a=t._attrs.x,o=e.split("\n"),s=o.length-1,l="";i.each(o,function(t,e){0===e?"alphabetic"===r?l+=''+t+"":"top"===r?l+=''+t+"":"middle"===r?l+=''+t+"":"bottom"===r?l+=''+t+"":"hanging"===r&&(l+=''+t+""):l+=''+t+""}),n.innerHTML=l}else n.innerHTML=e;else n.innerHTML=""},e._setClip=function(t,e){var n=t._cfg.el;if(e)if(n.hasAttribute("clip-path"))e._cfg.hasUpdate&&this._updateShape(e);else{this._createDom(e),this._updateShape(e);var i=this.context.addClip(e);n.setAttribute("clip-path","url(#"+i+")")}else n.removeAttribute("clip-path")},e._setColor=function(t,e,n){var i=t._cfg.el,r=this.context;if(n)if(n=n.trim(),/^[r,R,L,l]{1}[\s]*\(/.test(n)){var a=r.find("gradient",n);a||(a=r.addGradient(n)),i.setAttribute(l[e],"url(#"+a+")")}else if(/^[p,P]{1}[\s]*\(/.test(n)){var o=r.find("pattern",n);o||(o=r.addPattern(n)),i.setAttribute(l[e],"url(#"+o+")")}else i.setAttribute(l[e],n);else i.setAttribute(l[e],"none")},e._setShadow=function(t){var e=t._cfg.el,n=t._attrs,i={dx:n.shadowOffsetX,dy:n.shadowOffsetY,blur:n.shadowBlur,color:n.shadowColor};if(i.dx||i.dy||i.blur||i.color){var r=this.context.find("filter",i);r||(r=this.context.addShadow(i,this)),e.setAttribute("filter","url(#"+r+")")}else e.removeAttribute("filter")},t}();t.exports=h},function(t,e,n){var i=n(1),r=n(222),a=n(223),o=n(224),s=n(225),l=n(226),u=function(){function t(t){var e=document.createElementNS("http://www.w3.org/2000/svg","defs"),n=i.uniqueId("defs_");e.id=n,t.appendChild(e),this.children=[],this.defaultArrow={},this.el=e,this.canvas=t}var e=t.prototype;return e.find=function(t,e){for(var n=this.children,i=null,r=0;r'}),n}var r=n(1),a=/^l\s*\(\s*([\d.]+)\s*\)\s*(.*)/i,o=/^r\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)\s*(.*)/i,s=/[\d.]+:(#[^\s]+|[^\)]+\))/gi,l=function(){function t(t){var e=null,n=r.uniqueId("gradient_");return"l"===t.toLowerCase()[0]?function(t,e){var n,o,s=a.exec(t),l=r.mod(r.toRadian(parseFloat(s[1])),2*Math.PI),u=s[2];l>=0&&l<.5*Math.PI?(n={x:0,y:0},o={x:1,y:1}):.5*Math.PI<=l&&l';e.innerHTML=n},t}();t.exports=o},function(t,e,n){var i=n(1),r=function(){function t(t,e){var n=document.createElementNS("http://www.w3.org/2000/svg","marker"),r=i.uniqueId("marker_");n.setAttribute("id",r);var a=document.createElementNS("http://www.w3.org/2000/svg","path");return a.setAttribute("stroke","none"),a.setAttribute("fill",t.stroke||"#000"),n.appendChild(a),n.setAttribute("overflow","visible"),n.setAttribute("orient","auto-start-reverse"),this.el=n,this.child=a,this.id=r,this.cfg=t["marker-start"===e?"startArrow":"endArrow"],this.stroke=t.stroke||"#000",!0===this.cfg?this._setDefaultPath(e,a):this._setMarker(t.lineWidth,a),this}var e=t.prototype;return e.match=function(){return!1},e._setDefaultPath=function(t,e){var n=this.el;e.setAttribute("d","M0,0 L6,3 L0,6 L3,3Z"),n.setAttribute("refX",3),n.setAttribute("refY",3)},e._setMarker=function(t,e){var n=this.el,r=this.cfg.path,a=this.cfg.d;i.isArray(r)&&(r=r.map(function(t){return t.join(" ")}).join("")),e.setAttribute("d",r),n.appendChild(e),a&&n.setAttribute("refX",a/t)},e.update=function(t){var e=this.child;e.attr?e.attr("fill",t):e.setAttribute("fill",t)},t}();t.exports=r},function(t,e,n){var i=n(1),r=function(){function t(t){this.type="clip";var e=document.createElementNS("http://www.w3.org/2000/svg","clipPath");this.el=e,this.id=i.uniqueId("clip_"),e.id=this.id;var n=t._cfg.el;return e.appendChild(n.cloneNode(!0)),this.cfg=t,this}var e=t.prototype;return e.match=function(){return!1},e.remove=function(){var t=this.el;t.parentNode.removeChild(t)},t}();t.exports=r},function(t,e,n){var i=n(1),r=/^p\s*\(\s*([axyn])\s*\)\s*(.*)/i,a=function(){function t(t){function e(){console.log(l.width,l.height),n.setAttribute("width",l.width),n.setAttribute("height",l.height)}var n=document.createElementNS("http://www.w3.org/2000/svg","pattern");n.setAttribute("patternUnits","userSpaceOnUse");var a=document.createElementNS("http://www.w3.org/2000/svg","image");n.appendChild(a);var o=i.uniqueId("pattern_");n.id=o,this.el=n,this.id=o,this.cfg=t;var s=r.exec(t)[2];a.setAttribute("href",s);var l=new Image;return s.match(/^data:/i)||(l.crossOrigin="Anonymous"),l.src=s,l.complete?e():(l.onload=e,l.src=l.src),this}return t.prototype.match=function(t,e){return this.cfg===e},t}();t.exports=a},function(t,e){var n={svg:"svg",circle:"circle",rect:"rect",text:"text",path:"path",foreignObject:"foreignObject",polygon:"polygon",ellipse:"ellipse",image:"image"};t.exports=function(t,e,i){var r=i.target||i.srcElement;if(!n[r.tagName]){for(var a=r.parentNode;a&&!n[a.tagName];)a=a.parentNode;r=a}return this._cfg.el===r?this:this.find(function(t){return t._cfg&&t._cfg.el===r})}},function(t,e,n){t.exports={addEventListener:n(229),createDom:n(94),getBoundingClientRect:n(230),getHeight:n(231),getOuterHeight:n(232),getOuterWidth:n(233),getRatio:n(234),getStyle:n(235),getWidth:n(236),modifyCSS:n(95),requestAnimationFrame:n(96)}},function(t,e){t.exports=function(t,e,n){if(t){if(t.addEventListener)return t.addEventListener(e,n,!1),{remove:function(){t.removeEventListener(e,n,!1)}};if(t.attachEvent)return t.attachEvent("on"+e,n),{remove:function(){t.detachEvent("on"+e,n)}}}}},function(t,e){t.exports=function(t,e){if(t&&t.getBoundingClientRect){var n=t.getBoundingClientRect(),i=document.documentElement.clientTop,r=document.documentElement.clientLeft;return{top:n.top-i,bottom:n.bottom-i,left:n.left-r,right:n.right-r}}return e||null}},function(t,e){t.exports=function(t,e){var n=this.getStyle(t,"height",e);return"auto"===n&&(n=t.offsetHeight),parseFloat(n)}},function(t,e){t.exports=function(t,e){var n=this.getHeight(t,e),i=parseFloat(this.getStyle(t,"borderTopWidth"))||0,r=parseFloat(this.getStyle(t,"paddingTop"))||0,a=parseFloat(this.getStyle(t,"paddingBottom"))||0;return n+i+(parseFloat(this.getStyle(t,"borderBottomWidth"))||0)+r+a}},function(t,e){t.exports=function(t,e){var n=this.getWidth(t,e),i=parseFloat(this.getStyle(t,"borderLeftWidth"))||0,r=parseFloat(this.getStyle(t,"paddingLeft"))||0,a=parseFloat(this.getStyle(t,"paddingRight"))||0;return n+i+(parseFloat(this.getStyle(t,"borderRightWidth"))||0)+r+a}},function(t,e){t.exports=function(){return window.devicePixelRatio?window.devicePixelRatio:2}},function(t,e,n){var i=n(5);t.exports=function(t,e,n){try{return window.getComputedStyle?window.getComputedStyle(t,null)[e]:t.currentStyle[e]}catch(t){return i(n)?null:n}}},function(t,e){t.exports=function(t,e){var n=this.getStyle(t,"width",e);return"auto"===n&&(n=t.offsetWidth),parseFloat(n)}},function(t,e,n){t.exports={contains:n(41),difference:n(238),find:n(239),firstValue:n(240),flatten:n(241),flattenDeep:n(242),getRange:n(243),merge:n(42),pull:n(90),pullAt:n(130),reduce:n(244),remove:n(245),sortBy:n(246),union:n(247),uniq:n(131),valuesOfKey:n(64)}},function(t,e,n){var i=n(63),r=n(41);t.exports=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[];return i(t,function(t){return!r(e,t)})}},function(t,e,n){var i=n(11),r=n(26),a=n(128);t.exports=function(t,e){var n=void 0;if(i(e)&&(n=e),r(e)&&(n=function(t){return a(t,e)}),n)for(var o=0;o1&&void 0!==arguments[1]?arguments[1]:[];if(i(e))for(var r=0;re[i])return 1;if(t[i]1){var i=e[0].charAt(0);e.splice(1,0,e[0].substr(1)),e[0]=i}a(e,function(t,n){isNaN(t)||(e[n]=+t)}),t[n]=e}),t):void 0}},function(t,e,n){var i=n(4);t.exports=function(t){var e=0,n=0,r=0,a=0;return i(t)?1===t.length?e=n=r=a=t[0]:2===t.length?(e=r=t[0],n=a=t[1]):3===t.length?(e=t[0],n=a=t[1],r=t[2]):(e=t[0],n=t[1],r=t[2],a=t[3]):e=n=r=a=t,{r1:e,r2:n,r3:r,r4:a}}},function(t,e,n){var i=n(35);t.exports={clamp:n(50),fixedBase:n(256),isDecimal:n(257),isEven:n(258),isInteger:n(259),isNegative:n(260),isNumberEqual:i,isOdd:n(261),isPositive:n(262),maxBy:n(132),minBy:n(263),mod:n(93),snapEqual:i,toDegree:n(92),toInt:n(133),toInteger:n(133),toRadian:n(91)}},function(t,e){t.exports=function(t,e){var n=e.toString(),i=n.indexOf(".");if(-1===i)return Math.round(t);var r=n.substr(i+1).length;return r>20&&(r=20),parseFloat(t.toFixed(r))}},function(t,e,n){var i=n(9);t.exports=function(t){return i(t)&&t%1!=0}},function(t,e,n){var i=n(9);t.exports=function(t){return i(t)&&t%2==0}},function(t,e,n){var i=n(9),r=Number.isInteger?Number.isInteger:function(t){return i(t)&&t%1==0};t.exports=r},function(t,e,n){var i=n(9);t.exports=function(t){return i(t)&&t<0}},function(t,e,n){var i=n(9);t.exports=function(t){return i(t)&&t%2!=0}},function(t,e,n){var i=n(9);t.exports=function(t){return i(t)&&t>0}},function(t,e,n){var i=n(4),r=n(11),a=n(2);t.exports=function(t,e){if(i(t)){var n=t[0],o=void 0;o=r(e)?e(t[0]):t[0][e];var s=void 0;return a(t,function(t){(s=r(e)?e(t):t[e])1?1:u<0?0:u)/2,h=[-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816],f=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],p=0,g=0;g<12;g++){var d=c*h[g]+c,v=o(d,t,n,r,s),y=o(d,e,i,a,l),x=v*v+y*y;p+=f[g]*Math.sqrt(x)}return c*p},l=function(t,e,n,i,r,a,o,s){if(!(Math.max(t,n)Math.max(r,o)||Math.max(e,i)Math.max(a,s))){var l=(t-n)*(a-s)-(e-i)*(r-o);if(l){var u=((t*i-e*n)*(r-o)-(t-n)*(r*s-a*o))/l,c=((t*i-e*n)*(a-s)-(e-i)*(r*s-a*o))/l,h=+u.toFixed(2),f=+c.toFixed(2);if(!(h<+Math.min(t,n).toFixed(2)||h>+Math.max(t,n).toFixed(2)||h<+Math.min(r,o).toFixed(2)||h>+Math.max(r,o).toFixed(2)||f<+Math.min(e,i).toFixed(2)||f>+Math.max(e,i).toFixed(2)||f<+Math.min(a,s).toFixed(2)||f>+Math.max(a,s).toFixed(2)))return{x:u,y:c}}}},u=function(t,e,n){return e>=t.x&&e<=t.x+t.width&&n>=t.y&&n<=t.y+t.height},c=function(t,e,n,i){return null===t&&(t=e=n=i=0),null===e&&(e=t.y,n=t.width,i=t.height,t=t.x),{x:t,y:e,width:n,w:n,height:i,h:i,x2:t+n,y2:e+i,cx:t+n/2,cy:e+i/2,r1:Math.min(n,i)/2,r2:Math.max(n,i)/2,r0:Math.sqrt(n*n+i*i)/2,path:r(t,e,n,i),vb:[t,e,n,i].join(" ")}},h=function(t,e,n,r,a,o,s,l){i(t)||(t=[t,e,n,r,a,o,s,l]);var u=function(t,e,n,i,r,a,o,s){for(var l=[],u=[[],[]],c=void 0,h=void 0,f=void 0,p=void 0,g=0;g<2;++g)if(0===g?(h=6*t-12*n+6*r,c=-3*t+9*n-9*r+3*o,f=3*n-3*t):(h=6*e-12*i+6*a,c=-3*e+9*i-9*a+3*s,f=3*i-3*e),Math.abs(c)<1e-12){if(Math.abs(h)<1e-12)continue;(p=-f/h)>0&&p<1&&l.push(p)}else{var d=h*h-4*f*c,v=Math.sqrt(d);if(!(d<0)){var y=(-h+v)/(2*c);y>0&&y<1&&l.push(y);var x=(-h-v)/(2*c);x>0&&x<1&&l.push(x)}}for(var m=l.length,_=m,b=void 0;m--;)b=1-(p=l[m]),u[0][m]=b*b*b*t+3*b*b*p*n+3*b*p*p*r+p*p*p*o,u[1][m]=b*b*b*e+3*b*b*p*i+3*b*p*p*a+p*p*p*s;return u[0][_]=t,u[1][_]=e,u[0][_+1]=o,u[1][_+1]=s,u[0].length=u[1].length=_+2,{min:{x:Math.min.apply(0,u[0]),y:Math.min.apply(0,u[1])},max:{x:Math.max.apply(0,u[0]),y:Math.max.apply(0,u[1])}}}.apply(null,t);return c(u.min.x,u.min.y,u.max.x-u.min.x,u.max.y-u.min.y)},f=function(t,e,n,i,r,a,o,s,l){var u=1-l,c=Math.pow(u,3),h=Math.pow(u,2),f=l*l,p=f*l,g=t+2*l*(n-t)+f*(r-2*n+t),d=e+2*l*(i-e)+f*(a-2*i+e),v=n+2*l*(r-n)+f*(o-2*r+n),y=i+2*l*(a-i)+f*(s-2*a+i);return{x:c*t+3*h*l*n+3*u*l*l*r+p*o,y:c*e+3*h*l*i+3*u*l*l*a+p*s,m:{x:g,y:d},n:{x:v,y:y},start:{x:u*t+l*n,y:u*e+l*i},end:{x:u*r+l*o,y:u*a+l*s},alpha:90-180*Math.atan2(g-v,d-y)/Math.PI}},p=function(t,e,n){if(!function(t,e){return t=c(t),e=c(e),u(e,t.x,t.y)||u(e,t.x2,t.y)||u(e,t.x,t.y2)||u(e,t.x2,t.y2)||u(t,e.x,e.y)||u(t,e.x2,e.y)||u(t,e.x,e.y2)||u(t,e.x2,e.y2)||(t.xe.x||e.xt.x)&&(t.ye.y||e.yt.y)}(h(t),h(e)))return n?0:[];for(var i=~~(s.apply(0,t)/8),r=~~(s.apply(0,e)/8),a=[],o=[],p={},g=n?0:[],d=0;d=0&&P<=1&&T>=0&&T<=1&&(n?g++:g.push({x:k.x,y:k.y,t1:P,t2:T}))}}return g};t.exports=function(t,e){return function(t,e,n){t=a(t),e=a(e);for(var i=void 0,r=void 0,o=void 0,s=void 0,l=void 0,u=void 0,c=void 0,h=void 0,f=void 0,g=void 0,d=n?0:[],v=0,y=t.length;v=3&&(3===t.length&&e.push("Q"),e=e.concat(t[1])),2===t.length&&e.push("L"),e=e.concat(t[t.length-1])})}(t,e,i));else{var a=[].concat(t);"M"===a[0]&&(a[0]="L");for(var o=0;o<=i-1;o++)r.push(a)}return r}t.exports=function(t,e){if(1===t.length)return t;var n=t.length-1,r=e.length-1,a=n/r,o=[];if(1===t.length&&"M"===t[0][0]){for(var s=0;s=0;p--)l=s[p].index,"add"===s[p].type?t.splice(l,0,[].concat(t[l])):t.splice(l,1)}if((a=t.length)0)){t[a]=e[a];break}r=i(r,t[a-1],1)}t[a]=["Q"].concat(r.reduce(function(t,e){return t.concat(e)},[]));break;case"T":t[a]=["T"].concat(r[0]);break;case"C":if(r.length<3){if(!(a>0)){t[a]=e[a];break}r=i(r,t[a-1],2)}t[a]=["C"].concat(r.reduce(function(t,e){return t.concat(e)},[]));break;case"S":if(r.length<2){if(!(a>0)){t[a]=e[a];break}r=i(r,t[a-1],1)}t[a]=["S"].concat(r.reduce(function(t,e){return t.concat(e)},[]));break;default:t[a]=e[a]}return t}},function(t,e,n){var i={lc:n(275),lowerCase:n(142),lowerFirst:n(75),substitute:n(276),uc:n(277),upperCase:n(143),upperFirst:n(87)};t.exports=i},function(t,e,n){t.exports=n(142)},function(t,e){t.exports=function(t,e){return t&&e?t.replace(/\\?\{([^{}]+)\}/g,function(t,n){return"\\"===t.charAt(0)?t.slice(1):void 0===e[n]?"":e[n]}):t}},function(t,e,n){t.exports=n(143)},function(t,e,n){var i=n(12),r={getType:n(84),isArray:n(4),isArrayLike:n(13),isBoolean:n(82),isFunction:n(11),isNil:n(5),isNull:n(279),isNumber:n(9),isObject:n(24),isObjectLike:n(48),isPlainObject:n(26),isPrototype:n(85),isType:i,isUndefined:n(280),isString:n(10),isRegExp:n(281),isDate:n(80),isArguments:n(282),isError:n(283)};t.exports=r},function(t,e){t.exports=function(t){return null===t}},function(t,e){t.exports=function(t){return void 0===t}},function(t,e,n){var i=n(12);t.exports=function(t){return i(t,"RegExp")}},function(t,e,n){var i=n(12);t.exports=function(t){return i(t,"Arguments")}},function(t,e,n){var i=n(12);t.exports=function(t){return i(t,"Error")}},function(t,e){t.exports=function(t,e,n){var i=void 0;return function(){var r=this,a=arguments,o=n&&!i;clearTimeout(i),i=setTimeout(function(){i=null,n||t.apply(r,a)},e),o&&t.apply(r,a)}}},function(t,e,n){var i=n(13);t.exports=function(t,e){if(!i(t))return-1;var n=Array.prototype.indexOf;if(n)return n.call(t,e);for(var r=-1,a=0;ae?(i&&(clearTimeout(i),i=null),s=u,o=t.apply(r,a),i||(r=a=null)):i||!1===n.trailing||(i=setTimeout(l,c)),o};return u.cancel=function(){clearTimeout(i),s=0,i=r=a=null},u}},function(t,e,n){function i(t,e){var n,i,r=function(t){if(f.isEmpty(t))return null;var e=t[0].x,n=t[0].x,i=t[0].y,r=t[0].y;return f.each(t,function(t){e=e>t.x?t.x:e,n=nt.y?t.y:i,r=r0?o.maxX:o.minX,l,1];t.apply(u),t.attr({transform:[["t",-n,-l],["s",.01,1],["t",n,l]]});var c={transform:[["t",-n,-l],["s",100,1],["t",n,l]]},h=r(e,a,i);t.animate(c,h.duration,h.easing,h.callback,h.delay)}function s(t,e,n){var i,a,o=t._id,s=t.get("index");if(n.isPolar&&"point"!==t.name)i=n.getCenter().x,a=n.getCenter().y;else{var l=t.getBBox();i=(l.minX+l.maxX)/2,a=(l.minY+l.maxY)/2}var u=[i,a,1];t.apply(u),t.attr({transform:[["t",-i,-a],["s",.01,.01],["t",i,a]]});var c={transform:[["t",-i,-a],["s",100,100],["t",i,a]]},h=r(e,s,o);t.animate(c,h.duration,h.easing,h.callback,h.delay)}function l(t,e){if("path"===t.get("type")){var n=t._id,i=t.get("index"),a=g.pathToAbsolute(t.attr("path"));t.attr("path",[a[0]]);var o={path:a},s=r(e,i,n);t.animate(o,s.duration,s.easing,s.callback,s.delay)}}function u(t,e,n,i,a){var o,s=function(t){var e,n,i,r,a,o=t.start,s=t.end,l=t.getWidth(),u=t.getHeight();return t.isPolar?(r=t.getRadius(),i=t.getCenter(),e=t.startAngle,n=t.endAngle,(a=new p.Fan({attrs:{x:i.x,y:i.y,rs:0,re:r+200,startAngle:e,endAngle:e}})).endState={endAngle:n}):(a=new p.Rect({attrs:{x:o.x-200,y:s.y-200,width:t.isTransposed?l+400:0,height:t.isTransposed?0:u+400}}),t.isTransposed?a.endState={height:u+400}:a.endState={width:l+400}),a.isClip=!0,a}(n),l=t.get("canvas"),u=t._id,c=t.get("index");i?(s.attr("startAngle",i),s.attr("endAngle",i),o={endAngle:a}):o=s.endState,s.set("canvas",l),t.attr("clip",s),t.setSilent("animating",!0);var h=r(e,c,u);s.animate(o,h.duration,h.easing,function(){t&&!t.get("destroyed")&&(t.attr("clip",null),t.setSilent("cacheShape",null),t.setSilent("animating",!1),s.remove())},h.delay)}function c(t,e){var n=t._id,i=t.get("index"),a=f.isNil(t.attr("fillOpacity"))?1:t.attr("fillOpacity"),o=f.isNil(t.attr("strokeOpacity"))?1:t.attr("strokeOpacity");t.attr("fillOpacity",0),t.attr("strokeOpacity",0);var s={fillOpacity:a,strokeOpacity:o},l=r(e,i,n);t.animate(s,l.duration,l.easing,l.callback,l.delay)}function h(t,e,n){var r=i(t,n),a=r.endAngle;u(t,e,n,r.startAngle,a)}var f=n(0),p=n(16),g=f.PathUtil;t.exports={enter:{clipIn:u,zoomIn:s,pathIn:l,scaleInY:a,scaleInX:o,fanIn:h,fadeIn:c},leave:{lineWidthOut:function(t,e){var n={lineWidth:0,opacity:0},i=t._id,a=r(e,t.get("index"),i);t.animate(n,a.duration,a.easing,function(){t.remove()},a.delay)},zoomOut:function(t,e,n){var i,a,o=t._id,s=t.get("index");if(n.isPolar&&"point"!==t.name)i=n.getCenter().x,a=n.getCenter().y;else{var l=t.getBBox();i=(l.minX+l.maxX)/2,a=(l.minY+l.maxY)/2}var u=[i,a,1];t.apply(u);var c={transform:[["t",-i,-a],["s",.01,.01],["t",i,a]]},h=r(e,s,o);t.animate(c,h.duration,h.easing,function(){t.remove()},h.delay)},pathOut:function(t,e){if("path"===t.get("type")){var n=t._id,i=t.get("index"),a={path:[g.pathToAbsolute(t.attr("path"))[0]]},o=r(e,i,n);t.animate(a,o.duration,o.easing,function(){t.remove()},o.delay)}},fadeOut:function(t,e){var n=t._id,i={fillOpacity:0,strokeOpacity:0},a=r(e,t.get("index"),n);t.animate(i,a.duration,a.easing,function(){t.remove()},a.delay)}},appear:{clipIn:u,zoomIn:s,pathIn:l,scaleInY:a,scaleInX:o,fanIn:h,fadeIn:c},update:{fadeIn:c,fanIn:h}}},function(t,e,n){function i(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t,e,n){var i=(t-e)/(n-e);return i>=0&&i<=1}function a(t,e){var n=!1;if(t){if("theta"===t.type){var i=t.start,a=t.end;n=r(e.x,i.x,a.x)&&r(e.y,i.y,a.y)}else{var o=t.invert(e);n=o.x>=0&&o.y>=0&&o.x<=1&&o.y<=1}}return n}var o=n(148),s=n(20),l=n(0),u=n(165),c=n(7),h=n(151),f=n(355),p={};l.each(s,function(t,e){var n=l.lowerFirst(e);p[n]=function(e){var n=new t(e);return this.addGeom(n),n}});var g=function(t){function e(e){var n,r=i(i(n=t.call(this,e)||this));return r._setTheme(),l.each(s,function(t,e){var n=l.lowerFirst(e);r[n]=function(e){void 0===e&&(e={}),e.viewTheme=r.get("viewTheme");var n=new t(e);return r.addGeom(n),n}}),r.init(),n}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){return{viewContainer:null,coord:null,start:{x:0,y:0},end:{x:1,y:1},geoms:[],scales:{},options:{},scaleController:null,padding:0,theme:null,parent:null,tooltipEnable:!0,animate:c.animate,visible:!0}},n._setTheme=function(){var t=this.get("theme"),e={},n={};l.isObject(t)?n=t:-1!==l.indexOf(Object.keys(h),t)&&(n=h[t]),l.deepMix(e,c,n),this.set("viewTheme",e)},n.init=function(){this._initViewPlot(),this.get("data")&&this._initData(this.get("data")),this._initOptions(),this._initControllers(),this._bindEvents()},n._initOptions=function(){var t=this,e=l.mix({},t.get("options"));e.scales||(e.scales={}),e.coord||(e.coord={}),!1===e.animate&&this.set("animate",!1),(!1===e.tooltip||l.isNull(e.tooltip))&&this.set("tooltipEnable",!1),e.geoms&&e.geoms.length&&l.each(e.geoms,function(e){t._createGeom(e)});var n=t.get("scaleController");n&&(n.defs=e.scales);var i=t.get("coordController");i&&i.reset(e.coord),this.set("options",e)},n._createGeom=function(t){var e,n=t.type;this[n]&&(e=this[n](),l.each(t,function(t,n){if(e[n])if(l.isObject(t)&&t.field)if("label"===t)e[n](t.field,t.callback,t.cfg);else{var i;l.each(t,function(t,e){"field"!==e&&(i=t)}),e[n](t.field,i)}else e[n](t)}))},n._initControllers=function(){var t=this.get("options"),e=this.get("viewTheme"),n=this.get("canvas"),i=new u.Scale({viewTheme:e,defs:t.scales}),r=new u.Coord(t.coord);this.set("scaleController",i),this.set("coordController",r);var a=new u.Axis({canvas:n,viewTheme:e});this.set("axisController",a);var o=new u.Guide({viewTheme:e,options:t.guides||[]});this.set("guideController",o)},n._initViewPlot=function(){this.get("viewContainer")||this.set("viewContainer",this.get("middlePlot"))},n._initGeoms=function(){for(var t=this.get("geoms"),e=this.get("filteredData"),n=this.get("coord"),i=this.get("_id"),r=0;r0;){t.shift().destroy()}},n._drawGeoms=function(){this.emit("beforedrawgeoms");for(var t=this.get("geoms"),e=this.get("coord"),n=0;n0?r.change({min:0}):s<=0&&r.change({max:0}))}}},n._setCatScalesRange=function(){var t=this.get("coord"),e=this.get("viewTheme"),n=this.getXScale(),i=this.getYScales(),r=[];n&&r.push(n),r=r.concat(i);var a=t.isPolar&&function(t){var e=t.startAngle,n=t.endAngle;return!(!l.isNil(e)&&!l.isNil(n)&&n-e<2*Math.PI)}(t),o=this.get("scaleController").defs;l.each(r,function(n){if((n.isCategory||n.isIdentity)&&n.values&&(!o[n.field]||!o[n.field].range)){var i,r=n.values.length;if(1===r)i=[.5,1];else{var s=0;i=a?t.isTransposed?[(s=1/r*e.widthRatio.multiplePie)/2,1-s/2]:[0,1-1/r]:[s=1/r*1/2,1-s]}n.range=i}})},n.getXScale=function(){var t=this.get("geoms"),e=null;return l.isEmpty(t)||(e=t[0].getXScale()),e},n.getYScales=function(){for(var t=this.get("geoms"),e=[],n=0;n=0?"positive":"negative";o[d][g]||(o[d][g]=0),h[n]=[o[d][g],p+o[d][g]],o[d][g]+=p}}},e}(a);a.Stack=o,t.exports=o},function(t,e,n){var i={merge:n(42),values:n(64)},r=n(144),a=n(2);t.exports={processAdjust:function(t){var e=i.merge(t),n=this.dodgeBy,a=t;n&&(a=r(e,n)),this.cacheMap={},this.adjDataArray=a,this.mergeData=e,this.adjustData(a,e),this.adjDataArray=null,this.mergeData=null},getDistribution:function(t){var e=this.adjDataArray,n=this.cacheMap,r=n[t];return r||(r={},a(e,function(e,n){var o=i.values(e,t);o.length||o.push(0),a(o,function(t){r[t]||(r[t]=[]),r[t].push(n)})}),n[t]=r),r},adjustDim:function(t,e,n,i,r){var o=this,s=o.getDistribution(t),l=o.groupData(n,t);a(l,function(n,i){i=parseFloat(i);var l;l=1===e.length?{pre:e[0]-1,next:e[0]+1}:o.getAdjustRange(t,i,e),a(n,function(e){var n=e[t],i=s[n],a=i.indexOf(r);e[t]=o.getDodgeOffset(l,a,i.length)})})}}},function(t,e){t.exports={_initDefaultCfg:function(){this.xField=null,this.yField=null,this.height=null,this.size=10,this.reverseOrder=!1,this.adjustNames=["y"]},processOneDimStack:function(t){var e=this.xField,n=this.yField||"y",i=this.height,r={};this.reverseOrder&&(t=t.slice(0).reverse());for(var a=0,o=t.length;ai.width||n.height>i.height?r.push(t[a]):n.width*n.height>i.width*i.height&&r.push(t[a]);for(var o=0;o0?e="left":t[0]<0&&(e="right"),e},n.getLinePath=function(){var t=this.get("center"),e=t.x,n=t.y,i=this.get("radius"),r=i,a=this.get("startAngle"),o=this.get("endAngle"),s=this.get("inner"),l=[];if(Math.abs(o-a)===2*Math.PI)l=[["M",e,n],["m",0,-r],["a",i,r,0,1,1,0,2*r],["a",i,r,0,1,1,0,-2*r],["z"]];else{var u=this._getCirclePoint(a),c=this._getCirclePoint(o),h=Math.abs(o-a)>Math.PI?1:0,f=a>o?0:1;if(s){var p=this.getSideVector(s*i,u),g=this.getSideVector(s*i,c),d={x:p[0]+e,y:p[1]+n},v={x:g[0]+e,y:g[1]+n};l=[["M",d.x,d.y],["L",u.x,u.y],["A",i,r,0,h,f,c.x,c.y],["L",v.x,v.y],["A",i*s,r*s,0,h,Math.abs(f-1),d.x,d.y]]}else l=[["M",e,n],["L",u.x,u.y],["A",i,r,0,h,f,c.x,c.y],["L",e,n]]}return l},n.addLabel=function(e,n,i){var r=this.get("label").offset||this.get("_labelOffset")||.001;n=this.getSidePoint(n,r),t.prototype.addLabel.call(this,e,n,i)},n.autoRotateLabels=function(){var t=this.get("ticks"),e=this.get("labelRenderer");if(e&&t.length>12){var n=this.get("radius"),r=this.get("startAngle"),a=this.get("endAngle")-r,o=a/(t.length-1),s=Math.sin(o/2)*n*2,l=this.getMaxLabelWidth(e);i.each(e.get("group").get("children"),function(e,n){var i=t[n].value*a+r,o=i%(2*Math.PI);lMath.PI&&(i-=Math.PI),i-=Math.PI/2,e.attr("textAlign","center")):o>Math.PI/2?i-=Math.PI:oo.x)&&(u=!0);var c=a.vertical([],l,u);return a.scale([],c,t*n)},n.getAxisVector=function(){var t=this.get("start"),e=this.get("end");return[e.x-t.x,e.y-t.y]},n.getLinePath=function(){var t=this.get("start"),e=this.get("end"),n=[];return n.push(["M",t.x,t.y]),n.push(["L",e.x,e.y]),n},n.getTickEnd=function(t,e){var n=this.getSideVector(e);return{x:t.x+n[0],y:t.y+n[1]}},n.getTickPoint=function(t){var e=this.get("start"),n=this.get("end"),i=n.x-e.x,r=n.y-e.y;return{x:e.x+i*t,y:e.y+r*t}},n.renderTitle=function(){var t=this.get("title"),e=this.getTickPoint(.5),n=t.offset;if(r.isNil(n)){n=20;var i=this.get("labelsGroup");if(i){n+=this.getMaxLabelWidth(i)+(this.get("label").offset||this.get("_labelOffset"))}}var o=t.textStyle,s=r.mix({},o);if(t.text){var l=this.getAxisVector();if(t.autoRotate&&r.isNil(o.rotate)){var u=0;if(!r.snapEqual(l[1],0)){var c=[l[0],l[1]];u=a.angleTo(c,[1,0],!0)}s.rotate=u*(180/Math.PI)}else r.isNil(o.rotate)||(s.rotate=o.rotate/180*Math.PI);var h,f=this.getSideVector(n),p=t.position;h="start"===p?{x:this.get("start").x+f[0],y:this.get("start").y+f[1]}:"end"===p?{x:this.get("end").x+f[0],y:this.get("end").y+f[1]}:{x:e.x+f[0],y:e.y+f[1]},s.x=h.x,s.y=h.y,s.text=t.text;var g=this.get("group").addShape("Text",{zIndex:2,attrs:s});g.name="axis-title",this.get("appendInfo")&&g.setSilent("appendInfo",this.get("appendInfo"))}},n.autoRotateLabels=function(){var t=this.get("labelRenderer"),e=this.get("title");if(t){var n=t.get("group").get("children"),i=this.get("label").offset,a=e?e.offset:48;if(a<0)return;var o,s,l=this.getAxisVector();if(r.snapEqual(l[0],0)&&e&&e.text)(s=this.getMaxLabelWidth(t))>a-i-12&&(o=-1*Math.acos((a-i-12)/s));else if(r.snapEqual(l[1],0)&&n.length>1){var u=Math.abs(this._getAvgLabelLength(t));(s=this.getMaxLabelWidth(t))>u&&(o=Math.asin(1.25*(a-i-12)/s))}if(o){var c=this.get("factor");r.each(n,function(t){t.rotateAtStart(o),r.snapEqual(l[1],0)&&(c>0?t.attr("textAlign","left"):t.attr("textAlign","right"))})}}},n.autoHideLabels=function(){var t,e,n=this.get("labelRenderer");if(n){var i=n.get("group").get("children"),a=this.getAxisVector();if(i.length<2)return;if(r.snapEqual(a[0],0)){var o=this.getMaxLabelHeight(n)+8,s=Math.abs(this._getAvgLabelHeightSpace(n));o>s&&(t=o,e=s)}else if(r.snapEqual(a[1],0)&&i.length>1){var l=this.getMaxLabelWidth(n)+8,u=Math.abs(this._getAvgLabelLength(n));l>u&&(t=l,e=u)}if(t&&e){var c=Math.ceil(t/e);r.each(i,function(t,e){e%c!=0&&t.attr("text","")})}}},e}(i);t.exports=o},function(t,e,n){var i=n(3),r=n(31),a=i.MatrixUtil,o=i.PathUtil,s=a.vec2,l=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{type:"polyline"})},n.getLinePath=function(){var t=this.get("tickPoints"),e=this.get("start"),n=this.get("end"),r=[];r.push(e.x),r.push(e.y),i.each(t,function(t){r.push(t.x),r.push(t.y)}),r.push(n.x),r.push(n.y);var a=o.catmullRomToBezier(r);return a.unshift(["M",e.x,e.y]),a},n.getTickPoint=function(t,e){return this.get("tickPoints")[e]},n.getTickEnd=function(t,e,n){var i=this.get("tickLine"),r=e||i.length,a=this.getSideVector(r,t,n);return{x:t.x+a[0],y:t.y+a[1]}},n.getSideVector=function(t,e,n){var i;if(0===n){if((i=this.get("start")).x===e.x&&i.y===e.y)return[0,0]}else{i=this.get("tickPoints")[n-1]}var r=[e.x-i.x,e.y-i.y],a=s.normalize([],r),o=s.vertical([],a,!1);return s.scale([],o,t)},e}(r);t.exports=l},function(t,e,n){t.exports={Guide:n(15),Arc:n(315),DataMarker:n(316),DataRegion:n(317),Html:n(318),Image:n(319),Line:n(320),Region:n(321),Text:n(322)}},function(t,e,n){function i(t,e){var n,i=t.x-e.x,r=t.y-e.y;return 0===r?n=i<0?o/2:270*o/180:i>=0&&r>0?n=2*o-s(i/r):i<=0&&r<0?n=o-s(i/r):i>0&&r<0?n=o+s(-i/r):i<0&&r>0&&(n=s(i/-r)),n}var r=n(3),a=n(15),o=Math.PI,s=Math.atan,l=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return r.mix({},e,{name:"arc",start:null,end:null,style:{stroke:"#999",lineWidth:1}})},n.render=function(t,e){var n,a=this.parsePoint(t,this.get("start")),s=this.parsePoint(t,this.get("end")),l=t.getCenter(),u=Math.sqrt((a.x-l.x)*(a.x-l.x)+(a.y-l.y)*(a.y-l.y)),c=i(a,l),h=i(s,l);if(ho?1:0;n=[["M",a.x,a.y],["A",u,u,0,f,1,s.x,s.y]]}var p=e.addShape("path",{zIndex:this.get("zIndex"),attrs:r.mix({path:n},this.get("style"))});p.name="guide-arc",this.get("appendInfo")&&p.setSilent("appendInfo",this.get("appendInfo")),this.set("el",p)},e}(a);t.exports=l},function(t,e,n){var i=n(3),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{name:"dataMarker",zIndex:1,top:!0,position:null,style:{point:{r:3,fill:"#FFFFFF",stroke:"#1890FF",lineWidth:2},line:{stroke:"#A3B1BF",lineWidth:1},text:{fill:"#000000",opacity:.65,fontSize:12,textAlign:"start"}},display:{point:!0,line:!0,text:!0},lineLength:20,direction:"upward",autoAdjust:!0})},n.render=function(t,e){var n=this.parsePoint(t,this.get("position")),i=e.addGroup();i.name="guide-data-marker";var r,a,o=this._getElementPosition(n),s=this.get("display");if(s.line){var l=o.line;r=this._drawLine(l,i)}if(s.text&&this.get("content")){var u=o.text;a=this._drawText(u,i)}if(s.point){var c=o.point;this._drawPoint(c,i)}if(this.get("autoAdjust")){var h=i.getBBox(),f=h.minX,p=h.minY,g=h.maxX,d=h.maxY,v=t.start,y=t.end;if(a){f<=v.x&&a.attr("textAlign","start"),g>=y.x&&a.attr("textAlign","end");var x=this.get("direction");if("upward"===x&&p<=y.y||"upward"!==x&&d>=v.y){var m,_;"upward"===x&&p<=y.y?(m="top",_=1):(m="bottom",_=-1),a.attr("textBaseline",m);var b=0;if(this.get("display").line){b=this.get("lineLength");var w=[["M",n.x,n.y],["L",n.x,n.y+b*_]];r.attr("path",w)}var S=n.y+(b+2)*_;a.attr("y",S)}}}this.get("appendInfo")&&i.setSilent("appendInfo",this.get("appendInfo")),this.set("el",i)},n._getElementPosition=function(t){var e=t.x,n=t.y,i=this.get("display").line?this.get("lineLength"):0,r=this.get("direction");this.get("style").text.textBaseline="upward"===r?"bottom":"top";var a="upward"===r?-1:1;return{point:{x:e,y:n},line:[{x:e,y:n},{x:e,y:i*a+n}],text:{x:e,y:(i+2)*a+n}}},n._drawLine=function(t,e){var n=this.get("style").line,r=[["M",t[0].x,t[0].y],["L",t[1].x,t[1].y]];return e.addShape("path",{attrs:i.mix({path:r},n)})},n._drawText=function(t,e){var n=this.get("style").text;return e.addShape("text",{attrs:i.mix({text:this.get("content")},n,t)})},n._drawPoint=function(t,e){var n=this.get("style").point;return e.addShape("circle",{attrs:i.mix({},n,t)})},e}(n(15));t.exports=r},function(t,e,n){var i=n(3),r=n(156),a=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{name:"dataRegion",start:null,end:null,content:"",style:{region:{lineWidth:0,fill:"#000000",opacity:.04},text:{textAlign:"center",textBaseline:"bottom",fontSize:12,fill:"rgba(0, 0, 0, .65)"}}})},n.render=function(t,e,n){var r=this.get("lineLength")||0,a=this._getRegionData(t,n);if(a.length){var o=this._getBBox(a),s=[];s.push(["M",a[0].x,o.yMin-r]);for(var l=0,u=a.length;l=n&&h.push(this.parsePoint(t,[g[s],g[l]])),g[s]===c)break}return h},n._getBBox=function(t){for(var e=[],n=[],r=0;r');a.appendChild(o);var s=this.get("htmlContent")||this.get("html");if(i.isFunction(s)){s=s(this.get("xScales"),this.get("yScales"))}var l=r.createDom(s);o.appendChild(l),r.modifyCSS(o,{position:"absolute"}),this._setDomPosition(o,l,n),this.set("el",o)},n._setDomPosition=function(t,e,n){var i=this.get("alignX"),a=this.get("alignY"),o=r.getOuterWidth(e),s=r.getOuterHeight(e),l={x:n.x,y:n.y};"middle"===i&&"top"===a?l.x-=Math.round(o/2):"middle"===i&&"bottom"===a?(l.x-=Math.round(o/2),l.y-=Math.round(s)):"left"===i&&"bottom"===a?l.y-=Math.round(s):"left"===i&&"middle"===a?l.y-=Math.round(s/2):"left"===i&&"top"===a?(l.x=n.x,l.y=n.y):"right"===i&&"bottom"===a?(l.x-=Math.round(o),l.y-=Math.round(s)):"right"===i&&"middle"===a?(l.x-=Math.round(o),l.y-=Math.round(s/2)):"right"===i&&"top"===a?l.x-=Math.round(o):(l.x-=Math.round(o/2),l.y-=Math.round(s/2));var u=this.get("offsetX");u&&(l.x+=u);var c=this.get("offsetY");c&&(l.y+=c),r.modifyCSS(t,{top:Math.round(l.y)+"px",left:Math.round(l.x)+"px",visibility:"visible",zIndex:this.get("zIndex")})},n.clear=function(){var t=this.get("el");t&&t.parentNode&&t.parentNode.removeChild(t)},e}(n(15));t.exports=a},function(t,e,n){var i=n(3),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{type:"image",start:null,end:null,src:null,offsetX:null,offsetY:null})},n.render=function(t,e){var n=this.parsePoint(t,this.get("start")),i={x:n.x,y:n.y};if(i.img=this.get("src"),this.get("end")){var r=this.parsePoint(t,this.get("end"));i.width=r.x-n.x,i.height=r.y-n.y}else i.width=this.get("width")||32,i.height=this.get("height")||32;this.get("offsetX")&&(i.x+=this.get("offsetX")),this.get("offsetY")&&(i.y+=this.get("offsetY"));var a=e.addShape("Image",{zIndex:1,attrs:i});a.name="guide-image",this.get("appendInfo")&&a.setSilent("appendInfo",this.get("appendInfo")),this.set("el",a)},e}(n(15));t.exports=r},function(t,e,n){var i=n(3),r=n(15),a=i.MatrixUtil.vec2,o=n(14).FONT_FAMILY,s=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{name:"line",start:null,end:null,lineStyle:{stroke:"#000",lineWidth:1},text:{position:"end",autoRotate:!0,style:{fill:"#999",fontSize:12,fontWeight:500,fontFamily:o},content:null}})},n.render=function(t,e){var n=this.parsePoint(t,this.get("start")),i=this.parsePoint(t,this.get("end")),r=e.addGroup({viewId:e.get("viewId")});this._drawLines(n,i,r);var a=this.get("text");a&&a.content&&this._drawText(n,i,r),this.set("el",r)},n._drawLines=function(t,e,n){var r=[["M",t.x,t.y],["L",e.x,e.y]],a=n.addShape("Path",{attrs:i.mix({path:r},this.get("lineStyle"))});a.name="guide-line",this.get("appendInfo")&&a.setSilent("appendInfo",this.get("appendInfo"))},n._drawText=function(t,e,n){var r,o=this.get("text"),s=o.position,l=o.style||{};((r="start"===s?0:"center"===s?.5:i.isString(s)&&-1!==s.indexOf("%")?parseInt(s,10)/100:i.isNumber(s)?s:1)>1||r<0)&&(r=1);var u={x:t.x+(e.x-t.x)*r,y:t.y+(e.y-t.y)*r};if(o.offsetX&&(u.x+=o.offsetX),o.offsetY&&(u.y+=o.offsetY),u.text=o.content,u=i.mix({},u,l),o.autoRotate&&i.isNil(l.rotate)){var c=a.angleTo([e.x-t.x,e.y-t.y],[1,0],1);u.rotate=c}else i.isNil(l.rotate)||(u.rotate=l.rotate*Math.PI/180);var h=n.addShape("Text",{attrs:u});h.name="guide-line-text",this.get("appendInfo")&&h.setSilent("appendInfo",this.get("appendInfo"))},e}(r);t.exports=s},function(t,e,n){var i=n(3),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{name:"region",zIndex:1,start:null,end:null,style:{lineWidth:0,fill:"#CCD7EB",opacity:.4}})},n.render=function(t,e){var n=this.get("style"),r=this._getPath(t),a=e.addShape("path",{zIndex:this.get("zIndex"),attrs:i.mix({path:r},n)});a.name="guide-region",this.get("appendInfo")&&a.setSilent("appendInfo",this.get("appendInfo")),this.set("el",a)},n._getPath=function(t){var e=this.parsePoint(t,this.get("start")),n=this.parsePoint(t,this.get("end"));return[["M",e.x,e.y],["L",n.x,e.y],["L",n.x,n.y],["L",e.x,n.y],["z"]]},e}(n(15));t.exports=r},function(t,e,n){var i=n(3),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{name:"text",position:null,content:null,style:{fill:"#999",fontSize:12,fontWeight:500,textAlign:"center"},offsetX:null,offsetY:null,top:!0})},n.render=function(t,e){var n=this.parsePoint(t,this.get("position")),r=i.mix({},this.get("style")),a=this.get("offsetX"),o=this.get("offsetY");a&&(n.x+=a),o&&(n.y+=o),r.rotate&&(r.rotate=r.rotate*Math.PI/180);var s=e.addShape("Text",{zIndex:this.get("zIndex"),attrs:i.mix({text:this.get("content")},r,n)});s.name="guide-text",this.get("appendInfo")&&s.setSilent("appendInfo",this.get("appendInfo")),this.set("el",s)},e}(n(15));t.exports=r},function(t,e,n){var i=n(154);t.exports=i},function(t,e,n){t.exports={Category:n(157),CatHtml:n(159),CatPageHtml:n(325),Color:n(326),Size:n(328),CircleSize:n(329)}},function(t,e,n){function i(t,e){return t.getElementsByClassName(e)[0]}var r=n(3),a=n(159),o=n(14).FONT_FAMILY,s=r.DomUtil,l=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return r.mix({},e,{type:"category-page-legend",container:null,caretStyle:{fill:"rgba(0,0,0,0.65)"},pageNumStyle:{display:"inline-block",fontSize:"12px",fontFamily:o,cursor:"default"},slipDomStyle:{width:"auto",height:"auto",position:"absolute"},slipTpl:'

      1

      /2

      ',slipWidth:65,legendOverflow:"unset"})},n.render=function(){t.prototype._renderHTML.call(this),this._renderFlipPage()},n._renderFlipPage=function(){var t=document.getElementsByClassName("g2-legend")[0],e=i(t,"g2-legend-list"),n=this.get("position"),a=this.get("layout"),o="right"===n||"left"===n||"vertical"===a?"block":"inline-block";if(t.scrollHeight>t.offsetHeight){var l=this.get("slipTpl"),u=s.createDom(l),c=i(u,"g2-caret-up"),h=i(u,"g2-caret-down");s.modifyCSS(c,this.get("caretStyle")),s.modifyCSS(c,{fill:"rgba(0,0,0,0.25)"}),s.modifyCSS(h,this.get("caretStyle"));var f=i(u,"cur-pagenum"),p=i(u,"next-pagenum"),g=this.get("pageNumStyle");s.modifyCSS(f,r.mix({},g,{paddingLeft:"10px"})),s.modifyCSS(p,r.mix({},g,{opacity:.3,paddingRight:"10px"})),s.modifyCSS(u,r.mix({},this.get("slipDomStyle"),{top:t.offsetHeight+"px"})),t.style.overflow=this.get("legendOverflow"),t.appendChild(u);for(var d=e.childNodes,v=0,y=1,x=[],m=0;m=t.offsetHeight&&(y++,x.forEach(function(t){t.style.display="none"}),x=[]),x.push(d[m]);p.innerText="/"+y,d.forEach(function(e){e.style.display=o,(v=e.offsetTop+e.offsetHeight)>t.offsetHeight&&(e.style.display="none")}),c.addEventListener("click",function(){if(d[0].style.display!==o){var e=-1;d.forEach(function(t,n){t.style.display===o&&(e=-1===e?n:e,t.style.display="none")});for(var n=e-1;n>=0&&(d[n].style.display=o,v=d[e-1].offsetTop+d[e-1].offsetHeight,d[n].style.display="none",v0){var g=i.toRGB(l[p-1].color);u+=1-l[p].percentage+":"+g+" "}h.addShape("text",{attrs:r.mix({},{x:a+this.get("textOffset")/2,y:o-l[p].percentage*o,text:this._formatItemValue(l[p].value)+""},this.get("textStyle"),{textAlign:"start"})})}}else{u+="l (0) ";for(var d=0;d0){var v=i.toRGB(l[d-1].color);u+=l[d].percentage+":"+v+" "}u+=l[d].percentage+":"+n+" ",h.addShape("text",{attrs:r.mix({},{x:l[d].percentage*a,y:o+5+this.get("textOffset"),text:this._formatItemValue(l[d].value)+""},this.get("textStyle"))})}}h.addShape("rect",{attrs:{x:0,y:0,width:a,height:o,fill:u,strokeOpacity:0}}),h.addShape("path",{attrs:r.mix({path:c},this.get("lineStyle"))}),h.move(0,e)},e}(n(67));t.exports=a},function(t,e,n){var i=n(3),r=i.DomUtil,a=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){return{range:null,middleAttr:{fill:"#fff",fillOpacity:0},backgroundElement:null,minHandleElement:null,maxHandleElement:null,middleHandleElement:null,currentTarget:null,layout:"vertical",width:null,height:null,pageX:null,pageY:null}},n._beforeRenderUI=function(){var t=this.get("layout"),e=this.get("backgroundElement"),n=this.get("minHandleElement"),i=this.get("maxHandleElement"),r=this.addShape("rect",{attrs:this.get("middleAttr")}),a="vertical"===t?"ns-resize":"ew-resize";this.add([e,n,i]),this.set("middleHandleElement",r),e.set("zIndex",0),r.set("zIndex",1),n.set("zIndex",2),i.set("zIndex",2),r.attr("cursor","move"),n.attr("cursor",a),i.attr("cursor",a),this.sort()},n._renderUI=function(){"horizontal"===this.get("layout")?this._renderHorizontal():this._renderVertical()},n._transform=function(t){var e=this.get("range"),n=e[0]/100,i=e[1]/100,r=this.get("width"),a=this.get("height"),o=this.get("minHandleElement"),s=this.get("maxHandleElement"),l=this.get("middleHandleElement");o.resetMatrix(),s.resetMatrix(),"horizontal"===t?(l.attr({x:r*n,y:0,width:(i-n)*r,height:a}),o.translate(n*r,a),s.translate(i*r,a)):(l.attr({x:0,y:a*(1-i),width:r,height:(i-n)*a}),o.translate(1,(1-n)*a),s.translate(1,(1-i)*a))},n._renderHorizontal=function(){this._transform("horizontal")},n._renderVertical=function(){this._transform("vertical")},n._bindUI=function(){this.on("mousedown",i.wrapBehavior(this,"_onMouseDown"))},n._isElement=function(t,e){var n=this.get(e);if(t===n)return!0;if(n.isGroup){return n.get("children").indexOf(t)>-1}return!1},n._getRange=function(t,e){var n=t+e;return n=n>100?100:n,n=n<0?0:n},n._updateStatus=function(t,e){var n="x"===t?this.get("width"):this.get("height");t=i.upperFirst(t);var r,a=this.get("range"),o=this.get("page"+t),s=this.get("currentTarget"),l=this.get("rangeStash"),u="vertical"===this.get("layout")?-1:1,c=e["page"+t],h=(c-o)/n*100*u;a[1]<=a[0]?(this._isElement(s,"minHandleElement")||this._isElement(s,"maxHandleElement"))&&(a[0]=this._getRange(h,a[0]),a[1]=this._getRange(h,a[0])):(this._isElement(s,"minHandleElement")&&(a[0]=this._getRange(h,a[0])),this._isElement(s,"maxHandleElement")&&(a[1]=this._getRange(h,a[1]))),this._isElement(s,"middleHandleElement")&&(r=l[1]-l[0],a[0]=this._getRange(h,a[0]),a[1]=a[0]+r,a[1]>100&&(a[1]=100,a[0]=a[1]-r)),this.emit("sliderchange",{range:a}),this.set("page"+t,c),this._renderUI(),this.get("canvas").draw()},n._onMouseDown=function(t){var e=t.currentTarget,n=t.event,i=this.get("range");n.stopPropagation(),n.preventDefault(),this.set("pageX",n.pageX),this.set("pageY",n.pageY),this.set("currentTarget",e),this.set("rangeStash",[i[0],i[1]]),this._bindCanvasEvents()},n._bindCanvasEvents=function(){var t=this.get("canvas").get("containerDOM");this.onMouseMoveListener=r.addEventListener(t,"mousemove",i.wrapBehavior(this,"_onCanvasMouseMove")),this.onMouseUpListener=r.addEventListener(t,"mouseup",i.wrapBehavior(this,"_onCanvasMouseUp")),this.onMouseLeaveListener=r.addEventListener(t,"mouseleave",i.wrapBehavior(this,"_onCanvasMouseUp"))},n._onCanvasMouseMove=function(t){if(!this._mouseOutArea(t)){"horizontal"===this.get("layout")?this._updateStatus("x",t):this._updateStatus("y",t)}},n._onCanvasMouseUp=function(){this._removeDocumentEvents()},n._removeDocumentEvents=function(){this.onMouseMoveListener.remove(),this.onMouseUpListener.remove()},n._mouseOutArea=function(t){var e=this.get("canvas").get("el").getBoundingClientRect(),n=this.get("parent"),i=n.getBBox(),r=n.attr("matrix")[6],a=n.attr("matrix")[7],o=r+i.width,s=a+i.height,l=t.clientX-e.x,u=t.clientY-e.y;return lo||us},e}(i.Group);t.exports=a},function(t,e,n){var i=n(3),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{type:"size-legend",width:100,height:200,_unslidableElementStyle:{fill:"#4E7CCC",fillOpacity:1},frontMiddleBarStyle:{fill:"rgb(64, 141, 251)"}})},n._renderSliderShape=function(){var t=this.get("slider").get("backgroundElement"),e=this.get("layout"),n=this.get("width"),r=this.get("height"),a=this.get("height")/2,o=this.get("frontMiddleBarStyle"),s="vertical"===e?[[0,0],[n,0],[n,r],[n-4,r]]:[[0,a+r/2],[0,a+r/2-4],[n,a-r/2],[n,a+r/2]];return this._addMiddleBar(t,"Polygon",i.mix({points:s},o))},n._renderUnslidable=function(){var t=this.get("layout"),e=this.get("width"),n=this.get("height"),r=this.get("frontMiddleBarStyle"),a="vertical"===t?[[0,0],[e,0],[e,n],[e-4,n]]:[[0,n],[0,n-4],[e,0],[e,n]];this.get("group").addGroup().addShape("Polygon",{attrs:i.mix({points:a},r)});var o=this._formatItemValue(this.get("firstItem").value),s=this._formatItemValue(this.get("lastItem").value);"vertical"===this.get("layout")?(this._addText(e+10,n-3,o),this._addText(e+10,3,s)):(this._addText(0,n,o),this._addText(e,n,s))},n._addText=function(t,e,n){var r=this.get("group").addGroup(),a=this.get("textStyle"),o=this.get("titleShape"),s=this.get("titleGap");o&&(s+=o.getBBox().height),"vertical"===this.get("layout")?r.addShape("text",{attrs:i.mix({x:t+this.get("textOffset"),y:e,text:0===n?"0":n},a)}):(e+=s+this.get("textOffset")-20,o||(e+=10),r.addShape("text",{attrs:i.mix({x:t,y:e,text:0===n?"0":n},a)}))},e}(n(67));t.exports=r},function(t,e,n){var i=n(3),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{type:"size-circle-legend",width:100,height:200,_unslidableCircleStyle:{stroke:"rgb(99, 161, 248)",fill:"rgb(99, 161, 248)",fillOpacity:.3,lineWidth:1.5},triggerAttr:{fill:"white",shadowOffsetX:-2,shadowOffsetY:2,shadowBlur:10,shadowColor:"#ccc"},frontMiddleBarStyle:{fill:"rgb(64, 141, 251)"}})},n._renderSliderShape=function(){var t=this.get("slider").get("backgroundElement"),e=this.get("layout"),n="vertical"===e?2:this.get("width"),r="vertical"===e?this.get("height"):2,a=this.get("height")/2,o=this.get("frontMiddleBarStyle"),s="vertical"===e?[[0,0],[n,0],[n,r],[0,r]]:[[0,a+r],[0,a-r],[5+n-4,a-r],[5+n-4,a+r]];return this._addMiddleBar(t,"Polygon",i.mix({points:s},o))},n._addHorizontalTrigger=function(t,e,n,r){var a=this.get("slider").get(t+"HandleElement"),o=-this.get("height")/2,s=a.addShape("circle",{attrs:i.mix({x:0,y:o,r:r},e)}),l=a.addShape("text",{attrs:i.mix(n,{x:0,y:o+r+10,textAlign:"center",textBaseline:"middle"})}),u="vertical"===this.get("layout")?"ns-resize":"ew-resize";s.attr("cursor",u),l.attr("cursor",u),this.set(t+"ButtonElement",s),this.set(t+"TextElement",l)},n._addVerticalTrigger=function(t,e,n,r){var a=this.get("slider").get(t+"HandleElement"),o=a.addShape("circle",{attrs:i.mix({x:0,y:0,r:r},e)}),s=a.addShape("text",{attrs:i.mix(n,{x:r+10,y:0,textAlign:"start",textBaseline:"middle"})}),l="vertical"===this.get("layout")?"ns-resize":"ew-resize";o.attr("cursor",l),s.attr("cursor",l),this.set(t+"ButtonElement",o),this.set(t+"TextElement",s)},n._renderTrigger=function(){var t=this.get("firstItem"),e=this.get("lastItem"),n=this.get("layout"),r=this.get("textStyle"),a=this.get("triggerAttr"),o=i.mix({},a),s=i.mix({},a),l=i.mix({text:this._formatItemValue(t.value)+""},r),u=i.mix({text:this._formatItemValue(e.value)+""},r);"vertical"===n?(this._addVerticalTrigger("min",o,l,5),this._addVerticalTrigger("max",s,u,16)):(this._addHorizontalTrigger("min",o,l,5),this._addHorizontalTrigger("max",s,u,16))},n._bindEvents=function(){var t=this;if(this.get("slidable")){this.get("slider").on("sliderchange",function(e){var n=e.range,i=t.get("firstItem").value,r=t.get("lastItem").value,a=i+n[0]/100*(r-i),o=i+n[1]/100*(r-i),s=5+n[0]/100*11,l=5+n[1]/100*11;t._updateElement(a,o,s,l);var u=new Event("itemfilter",e,!0,!0);u.range=[a,o],t.emit("itemfilter",u)})}},n._updateElement=function(e,n,i,r){t.prototype._updateElement.call(this,e,n);var a=this.get("minTextElement"),o=this.get("maxTextElement"),s=this.get("minButtonElement"),l=this.get("maxButtonElement");s.attr("r",i),l.attr("r",r);if("vertical"===this.get("layout"))a.attr("x",i+10),o.attr("x",r+10);else{var u=-this.get("height")/2;a.attr("y",u+i+10),o.attr("y",u+r+10)}},n._addCircle=function(t,e,n,r,a){var o=this.get("group").addGroup(),s=this.get("_unslidableCircleStyle"),l=this.get("textStyle"),u=this.get("titleShape"),c=this.get("titleGap");u&&(c+=u.getBBox().height),o.addShape("circle",{attrs:i.mix({x:t,y:e+c,r:0===n?1:n},s)}),"vertical"===this.get("layout")?o.addShape("text",{attrs:i.mix({x:a+20+this.get("textOffset"),y:e+c,text:0===r?"0":r},l)}):o.addShape("text",{attrs:i.mix({x:t,y:e+c+a+13+this.get("textOffset"),text:0===r?"0":r},l)})},n._renderUnslidable=function(){var t=this.get("firstItem").value,e=this.get("lastItem").value;if(t>e){var n=e;e=t,t=n}var i=this._formatItemValue(t),r=this._formatItemValue(e),a=t<5?5:t,o=e>16?16:e;a>o&&(a=5,o=16),"vertical"===this.get("layout")?(this._addCircle(o,o,a,i,2*o),this._addCircle(o,2*o+16+a,o,r,2*o)):(this._addCircle(o,o,a,i,2*o),this._addCircle(2*o+16+a,o,o,r,2*o))},n.activate=function(e){this.get("slidable")&&t.prototype.activate.call(this,e)},e}(n(67));t.exports=r},function(t,e,n){var i=n(68);i.Html=n(331),i.Canvas=n(163),i.Mini=n(333),t.exports=i},function(t,e,n){function i(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t,e){return t.getElementsByClassName(e)[0]}var a=n(68),o=n(3),s=o.DomUtil,l=n(332),u=n(160),c=n(161),h=n(162),f=function(t){function e(e){var n;n=t.call(this,e)||this,o.assign(i(i(n)),c),o.assign(i(i(n)),h);var r=l;n.style=function(t,e){return Object.keys(t).forEach(function(n){e[n]&&(t[n]=o.mix(t[n],e[n]))}),t}(r,e),n._init_(),n.get("items")&&n.render();var a=n.get("crosshairs");if(a){var s="rect"===a.type?n.get("backPlot"):n.get("frontPlot"),f=new u(o.mix({plot:s,plotRange:n.get("plotRange"),canvas:n.get("canvas")},n.get("crosshairs")));f.hide(),n.set("crosshairGroup",f)}return n}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return o.mix({},e,{containerTpl:'
        ',itemTpl:'
      • {name}{value}
      • ',htmlContent:null,follow:!0,enterable:!1})},n._init_=function(){var t,e=this.get("containerTpl"),n=this.get("canvas").get("el").parentNode;if(!this.get("htmlContent")){if(/^\#/.test(e)){var i=e.replace("#","");t=document.getElementById(i)}else t=s.createDom(e),s.modifyCSS(t,this.style["g2-tooltip"]),n.appendChild(t),n.style.position="relative";this.set("container",t)}},n.render=function(){if(this.clear(),this.get("htmlContent")){var t=this.get("canvas").get("el").parentNode,e=this._getHtmlContent();t.appendChild(e),this.set("container",e)}else this._renderTpl()},n._renderTpl=function(){var t=this,e=t.get("showTitle"),n=t.get("titleContent"),i=t.get("container"),a=r(i,"g2-tooltip-title"),l=r(i,"g2-tooltip-list"),u=t.get("items");a&&e&&(s.modifyCSS(a,t.style["g2-tooltip-title"]),a.innerHTML=n),l&&(s.modifyCSS(l,t.style["g2-tooltip-list"]),o.each(u,function(e,n){l.appendChild(t._addItem(e,n))}))},n.clear=function(){var t=this.get("container");if(this.get("htmlContent"))t&&t.remove();else{var e=r(t,"g2-tooltip-title"),n=r(t,"g2-tooltip-list");e&&(e.innerHTML=""),n&&(n.innerHTML="")}},n.show=function(){var e=this.get("container");e.style.visibility="visible",e.style.display="block";var n=this.get("crosshairGroup");n&&n.show();var i=this.get("markerGroup");i&&i.show(),t.prototype.show.call(this),this.get("canvas").draw()},n.hide=function(){var e=this.get("container");e.style.visibility="hidden",e.style.display="none";var n=this.get("crosshairGroup");n&&n.hide();var i=this.get("markerGroup");i&&i.hide(),t.prototype.hide.call(this),this.get("canvas").draw()},n.destroy=function(){var e=this.get("container"),n=this.get("containerTpl");e&&!/^\#/.test(n)&&e.parentNode.removeChild(e);var i=this.get("crosshairGroup");i&&i.destroy();var r=this.get("markerGroup");r&&r.remove(),t.prototype.destroy.call(this)},n._addItem=function(t,e){var n=this.get("itemTpl"),i=o.substitute(n,o.mix({index:e},t)),a=s.createDom(i);s.modifyCSS(a,this.style["g2-tooltip-list-item"]);var l=r(a,"g2-tooltip-marker");l&&s.modifyCSS(l,this.style["g2-tooltip-marker"]);var u=r(a,"g2-tooltip-value");return u&&s.modifyCSS(u,this.style["g2-tooltip-value"]),a},n._getHtmlContent=function(){var t=this.get("htmlContent")(this.get("titleContent"),this.get("items"));return s.createDom(t)},n.setPosition=function(e,n,i){var r,a=this.get("container"),l=this.get("canvas").get("el"),u=s.getWidth(l),c=s.getHeight(l),h=a.clientWidth,f=a.clientHeight,p=e,g=n,d=this.get("prePosition")||{x:0,y:0};if(this.get("enterable"))r=[e,n-=a.clientHeight/2],d&&e-d.x>0?e-=a.clientWidth+1:e+=1;else if(this.get("position")){var v=a.clientWidth,y=a.clientHeight;e=(r=this._calcTooltipPosition(e,n,this.get("position"),v,y,i))[0],n=r[1]}else e=(r=this._constraintPositionInBoundary(e,n,h,f,u,c))[0],n=r[1];if(this.get("inPlot")){var x=this.get("plotRange");e=(r=this._constraintPositionInPlot(e,n,h,f,x,this.get("enterable")))[0],n=r[1]}var m=this.get("markerItems");o.isEmpty(m)||(p=m[0].x,g=m[0].y),this.set("prePosition",r);this.get("follow")&&(a.style.left=e+"px",a.style.top=n+"px");var _=this.get("crosshairGroup");if(_){var b=this.get("items");_.setPosition(p,g,b)}t.prototype.setPosition.call(this,e,n)},e}(a);t.exports=f},function(t,e,n){var i,r=n(14).FONT_FAMILY,a=(i={crosshairs:!1,offset:15},i["g2-tooltip"]={position:"absolute",visibility:"hidden",zIndex:8,transition:"visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1), left 0.4s cubic-bezier(0.23, 1, 0.32, 1), top 0.4s cubic-bezier(0.23, 1, 0.32, 1)",backgroundColor:"rgba(255, 255, 255, 0.9)",boxShadow:"0px 0px 10px #aeaeae",borderRadius:"3px",color:"rgb(87, 87, 87)",fontSize:"12px",fontFamily:r,lineHeight:"20px",padding:"10px 10px 6px 10px"},i["g2-tooltip-title"]={marginBottom:"4px"},i["g2-tooltip-list"]={margin:0,listStyleType:"none",padding:0},i["g2-tooltip-list-item"]={marginBottom:"4px"},i["g2-tooltip-marker"]={width:"5px",height:"5px",borderRadius:"50%",display:"inline-block",marginRight:"8px"},i["g2-tooltip-value"]={display:"inline-block",float:"right",marginLeft:"30px"},i);t.exports=a},function(t,e,n){var i=n(3),r=n(163),a=n(14).FONT_FAMILY,o=i.DomUtil,s=i.MatrixUtil,l=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{boardStyle:{x:0,y:0,width:0,height:0,radius:3},valueStyle:{x:0,y:0,text:"",fontFamily:a,fontSize:12,stroke:"#fff",lineWidth:2,fill:"black",textBaseline:"top",textAlign:"start"},padding:{top:5,right:5,bottom:0,left:5},triangleWidth:10,triangleHeight:4})},n._init_=function(){var t=this.get("padding"),e=this.get("frontPlot").addGroup();this.set("container",e);var n=e.addShape("rect",{attrs:i.mix({},this.get("boardStyle"))});this.set("board",n);var r=e.addShape("path",{attrs:{fill:this.get("boardStyle").fill}});this.set("triangleShape",r);var a=e.addGroup();a.move(t.left,t.top);var o=a.addShape("text",{attrs:i.mix({},this.get("valueStyle"))});this.set("valueShape",o)},n.render=function(){this.clear();var t=this.get("board"),e=this.get("valueShape"),n=this.get("padding"),i=this.get("items")[0];e&&e.attr("text",i.value);var r=e?e.getBBox():{width:80,height:30},a=n.left+r.width+n.right,o=n.top+r.height+n.bottom;t.attr("width",a),t.attr("height",o),this._centerTriangleShape()},n.clear=function(){this.get("valueShape").attr("text","")},n.setPosition=function(t,e,n){var i=this.get("container"),r=this.get("plotRange"),a=i.getBBox(),l=a.width,u=a.height;if(t-=l/2,n&&("point"===n.name||"interval"===n.name)){e=n.getBBox().y}if(e-=u,this.get("inPlot"))tr.tr.x?(t=r.tr.x-l,this._rightTriangleShape()):this._centerTriangleShape(),er.bl.y&&(e=r.bl.y-u);else{var c=this.get("canvas").get("el"),h=o.getWidth(c),f=o.getHeight(c);t<0?(t=0,this._leftTriangleShape()):t+l/2>h?(t=h-l,this._rightTriangleShape()):this._centerTriangleShape(),e<0?e=0:e+u>f&&(e=f-u)}var p=s.transform([1,0,0,0,1,0,0,0,1],[["t",t,e]]);i.stopAnimate(),i.animate({matrix:p},this.get("animationDuration"))},n._centerTriangleShape=function(){var t=this.get("triangleShape"),e=this.get("triangleWidth"),n=this.get("triangleHeight"),i=this.get("board").getBBox(),r=i.width,a=i.height,o=[["M",0,0],["L",e,0],["L",e/2,n],["L",0,0],["Z"]];t.attr("path",o),t.move(r/2-e/2,a-1)},n._leftTriangleShape=function(){var t=this.get("triangleShape"),e=this.get("triangleWidth"),n=this.get("triangleHeight"),i=this.get("board").getBBox().height,r=[["M",0,0],["L",e,0],["L",0,n+3],["L",0,0],["Z"]];t.attr("path",r),t.move(0,i-3)},n._rightTriangleShape=function(){var t=this.get("triangleShape"),e=this.get("triangleWidth"),n=this.get("triangleHeight"),i=this.get("board").getBBox(),r=i.width,a=i.height,o=[["M",0,0],["L",e,0],["L",e,n+4],["L",0,0],["Z"]];t.attr("path",o),t.move(r-e-1,a-4)},e}(r);t.exports=l},function(t,e,n){var i=n(0).MatrixUtil.vec2;t.exports={catmullRom2bezier:function(t,e,n){for(var r=!!e,a=[],o=0,s=t.length;o0&&(e=this._distribute(e,n)),t.prototype.adjustItems.call(this,e)},n._distribute=function(t,e){var n=this.get("coord"),i=n.getRadius(),r=this.get("label").labelHeight,a=n.getCenter(),o=2*(i+e)+2*r,s={start:n.start,end:n.end},l=this.get("geom");if(l){var u=l.get("view");s=u.getViewRegion()}var c=[[],[]];return t.forEach(function(t){t&&("right"===t.textAlign?c[0].push(t):c[1].push(t))}),c.forEach(function(t,e){var n=parseInt(o/r,10);t.length>n&&(t.sort(function(t,e){return e["..percent"]-t["..percent"]}),t.splice(n,t.length-n)),t.sort(function(t,e){return t.y-e.y}),function(t,e,n,i,r){var a,o=!0,s=n.start,l=n.end,u=Math.min(s.y,l.y),c=Math.abs(s.y-l.y),h=0,f=Number.MIN_VALUE,p=t.map(function(t){return t.y>h&&(h=t.y),t.yc&&(c=h-u);o;)for(p.forEach(function(t){var e=(Math.min.apply(f,t.targets)+Math.max.apply(f,t.targets))/2;t.pos=Math.min(Math.max(f,e-t.size/2),c-t.size)}),o=!1,a=p.length;a--;)if(a>0){var g=p[a-1],d=p[a];g.pos+g.size>d.pos&&(g.size+=d.size,g.targets=g.targets.concat(d.targets),g.pos+g.size>c&&(g.pos=c-g.size),p.splice(a,1),o=!0)}a=0,p.forEach(function(n){var i=u+e/2;n.targets.forEach(function(){t[a].y=n.pos+i,i+=e,a++})}),t.forEach(function(t){var e=t.r*t.r,n=Math.pow(Math.abs(t.y-i.y),2);if(e90&&(n-=180),n<-90&&(n+=180)),n/180*Math.PI},n.getLabelAlign=function(t){var e,n=this.get("coord").getCenter();e=t.angle<=Math.PI/2&&t.x>=n.x?"left":"right";return this.getDefaultOffset(t)<=0&&(e="right"===e?"left":"right"),e},n.getArcPoint=function(t){return t},n.getPointAngle=function(t){var e=this.get("coord"),n={x:r.isArray(t.x)?t.x[0]:t.x,y:t.y[0]};this.transLabelPoint(n);var i={x:r.isArray(t.x)?t.x[1]:t.x,y:t.y[1]};this.transLabelPoint(i);var a,s=o.getPointAngle(e,n);if(t.points&&t.points[0].y===t.points[1].y)a=s;else{var l=o.getPointAngle(e,i);s>=l&&(l+=2*Math.PI),a=s+(l-s)/2}return a},n.getCirclePoint=function(t,e){var n=this.get("coord"),r=n.getCenter(),a=n.getRadius()+e,o=i(r,t,a);return o.angle=t,o.r=a,o},e}(a);t.exports=l},function(t,e,n){var i=n(0),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);return e.prototype.setLabelPosition=function(t,e,n,r){i.isFunction(r)&&(r=r(t.text,e._origin,n));var a=this.get("coord"),o=a.isTransposed,s=a.convertPoint(e.points[0]),l=a.convertPoint(e.points[2]),u=(s.x-l.x)/2*(o?-1:1),c=(s.y-l.y)/2*(o?-1:1);switch(r){case"right":o?(t.x-=u,t.y+=c,t.textAlign=t.textAlign||"center"):(t.x-=u,t.y+=c,t.textAlign=t.textAlign||"left");break;case"left":o?(t.x-=u,t.y-=c,t.textAlign=t.textAlign||"center"):(t.x+=u,t.y+=c,t.textAlign=t.textAlign||"right");break;case"bottom":o?(t.x-=2*u,t.textAlign=t.textAlign||"left"):(t.y+=2*c,t.textAlign=t.textAlign||"center");break;case"middle":o?t.x-=u:t.y+=c,t.textAlign=t.textAlign||"center";break;case"top":t.textAlign=o?t.textAlign||"left":t.textAlign||"center"}},e}(n(65));t.exports=r},function(t,e,n){function i(t){return t.alias||t.field}var r=n(0),a=n(7).defaultColor,o={_getIntervalSize:function(t){var e=null,n=this.get("type"),i=this.get("coord");if(i.isRect&&("interval"===n||"schema"===n)){e=this.getSize(t._origin);var a=i.isTransposed?"y":"x";if(r.isArray(t[a])){e=e(1+i.rangeMax())/2&&(r=i.rangeMin()),e=i.invert(r),i.isCategory&&(e=i.translate(e)),e},_getOriginByPoint:function(t){var e=this.getXScale(),n=this.getYScale(),i=e.field,r=n.field,a=this.get("coord").invert(t),o=e.invert(a.x),s=n.invert(a.y),l={};return l[i]=o,l[r]=s,l},_getScale:function(t){var e=this.get("scales"),n=null;return r.each(e,function(e){if(e.field===t)return n=e,!1}),n},_getTipValueScale:function(){var t,e=this.getAttrsForLegend();r.each(e,function(e){var n=e.getScale(e.type);if(n.isLinear)return t=n,!1});var n=this.getXScale(),i=this.getYScale();return!t&&i&&"..y"===i.field?n:t||i||n},_getTipTitleScale:function(t){if(t)return this._getScale(t);var e,n=this.getAttr("position").getFields();return r.each(n,function(t){if(-1===t.indexOf(".."))return e=t,!1}),this._getScale(e)},_filterValue:function(t,e){var n=this.get("coord"),i=this.getYScale(),a=i.field,o=n.invert(e).y;o=i.invert(o);var s=t[t.length-1];return r.each(t,function(t){var e=t._origin;if(e[a][0]<=o&&e[a][1]>=o)return s=t,!1}),s},getXDistance:function(){var t=this.get("xDistance");if(!t){var e=this.getXScale();if(e.isCategory)t=1;else{var n=e.values,i=e.translate(n[0]),a=i;r.each(n,function(t){(t=e.translate(t))a&&(a=t)});var o=n.length;t=(a-i)/(o-1)}this.set("xDistance",t)}return t},findPoint:function(t,e){var n=this,i=n.get("type"),a=n.getXScale(),o=n.getYScale(),s=a.field,l=o.field,u=null;if(r.indexOf(["heatmap","point"],i)>-1){var c=n.get("coord").invert(t),h=a.invert(c.x),f=o.invert(c.y),p=1/0;return r.each(e,function(t){var e=Math.pow(t._origin[s]-h,2)+Math.pow(t._origin[l]-f,2);e=v){if(!_)return u=t,!1;r.isArray(u)||(u=[]),u.push(t)}}),r.isArray(u)&&(u=this._filterValue(u,t));else{var b;if(a.isLinear||"timeCat"===a.type){if((v>a.translate(m)||va.max||vMath.abs(a.translate(b._origin[s])-v)&&(d=b)}var A=n.getXDistance();return!u&&Math.abs(a.translate(d._origin[s])-v)<=A/2&&(u=d),u},getTipTitle:function(t,e){var n="",i=this._getTipTitleScale(e);if(i){var r=t[i.field];n=i.getText(r)}else if("heatmap"===this.get("type")){var a=this.getXScale(),o=this.getYScale();n="( "+a.getText(t[a.field])+", "+o.getText(t[o.field])+" )"}return n},getTipValue:function(t,e){var n,i=e.field,a=t.key;if(n=t[i],r.isArray(n)){var o=[];r.each(n,function(t){o.push(e.getText(t))}),n=o.join("-")}else n=e.getText(n,a);return n},getTipName:function(t){var e,n,a=this._getGroupScales();if(a.length&&r.each(a,function(t){return n=t,!1}),n){var o=n.field;e=n.getText(t[o])}else{e=i(this._getTipValueScale())}return e},getTipItems:function(t,e){function n(e,n,i){if(!r.isNil(n)&&""!==n){var o={title:c,point:t,name:e||c,value:n,color:t.color||a,marker:!0};o.size=l._getIntervalSize(t),f.push(r.mix({},o,i))}}var o,s,l=this,u=t._origin,c=l.getTipTitle(u,e),h=l.get("tooltipCfg"),f=[];if(h){var p=h.fields,g=h.cfg,d=[];if(r.each(p,function(t){d.push(u[t])}),g){r.isFunction(g)&&(g=g.apply(null,d));var v=r.mix({},{point:t,title:c,color:t.color||a,marker:!0},g);v.size=l._getIntervalSize(t),f.push(v)}else r.each(p,function(t){if(!r.isNil(u[t])){var e=l._getScale(t);o=i(e),s=e.getText(u[t]),n(o,s)}})}else{var y=l._getTipValueScale();r.isNil(u[y.field])||(s=l.getTipValue(u,y),n(o=l.getTipName(u),s))}return f},isShareTooltip:function(){var t,e=this.get("shareTooltip"),n=this.get("type"),i=this.get("view");if(t=i.get("parent")?i.get("parent").get("options"):i.get("options"),"interval"===n){var a=this.get("coord"),o=a.type;("theta"===o||"polar"===o&&a.isTransposed)&&(e=!1)}else this.getYScale()&&!r.inArray(["contour","point","polygon","edge"],n)||(e=!1);return t.tooltip&&r.isBoolean(t.tooltip.shared)&&(e=t.tooltip.shared),e}};t.exports=o},function(t,e,n){function i(t,e){if(!t)return!0;if(t.length!==e.length)return!0;var n=!1;return a.each(e,function(e,i){if(!function(t,e){if(a.isNil(t)||a.isNil(e))return!1;var n=t.get("origin"),i=e.get("origin");return a.isEqual(n,i)}(e,t[i]))return n=!0,!1}),n}function r(t,e){var n={};return a.each(t,function(t,i){var r=e.attr(i);a.isArray(r)&&(r=a.cloneDeep(r)),n[i]=r}),n}var a=n(0),o={_isAllowActive:function(){var t=this.get("allowActive");if(!a.isNil(t))return t;var e=this.get("view"),n=this.isShareTooltip();return!1===e.get("options").tooltip||!n},_onMouseenter:function(t){var e=t.shape,n=this.get("shapeContainer");e&&n.contain(e)&&this._isAllowActive()&&this.setShapesActived(e)},_onMouseleave:function(){var t=this.get("view").get("canvas");this.get("activeShapes")&&(this.clearActivedShapes(),t.draw())},_bindActiveAction:function(){var t=this.get("view"),e=this.get("type");t.on(e+":mouseenter",a.wrapBehavior(this,"_onMouseenter")),t.on(e+":mouseleave",a.wrapBehavior(this,"_onMouseleave"))},_offActiveAction:function(){var t=this.get("view"),e=this.get("type");t.off(e+":mouseenter",a.getWrapBehavior(this,"_onMouseenter")),t.off(e+":mouseleave",a.getWrapBehavior(this,"_onMouseleave"))},_setActiveShape:function(t){var e=this.get("activedOptions")||{},n=t.get("origin"),i=n.shape||this.getDefaultValue("shape");a.isArray(i)&&(i=i[0]);var o=this.get("shapeFactory"),s=a.mix({},t.attr(),{origin:n}),l=o.getActiveCfg(i,s);e.style&&a.mix(l,e.style);var u=r(l,t);t.setSilent("_originAttrs",u),e.animate?t.animate(l,300):t.attr(l),t.set("zIndex",1)},setShapesActived:function(t){var e=this;a.isArray(t)||(t=[t]);var n=e.get("activeShapes");if(i(n,t)){var r=e.get("view").get("canvas"),o=e.get("shapeContainer"),s=e.get("activedOptions");s&&s.highlight?(a.each(t,function(t){t.get("animating")&&t.stopAnimate()}),e.highlightShapes(t)):(n&&e.clearActivedShapes(),a.each(t,function(t){t.get("animating")&&t.stopAnimate(),t.get("visible")&&!t.get("selected")&&e._setActiveShape(t)})),e.set("activeShapes",t),o.sort(),r.draw()}},clearActivedShapes:function(){var t=this.get("shapeContainer"),e=this.get("activedOptions"),n=e&&e.animate;if(t&&!t.get("destroyed")){var i=this.get("activeShapes");a.each(i,function(t){if(!t.get("selected")){var e=t.get("_originAttrs");n?(t.stopAnimate(),t.animate(e,300)):t.attr(e),t.setZIndex(0),t.set("_originAttrs",null)}});if(this.get("preHighlightShapes")){var r=t.get("children");a.each(r,function(t){if(!t.get("selected")){var e=t.get("_originAttrs");e&&(n?(t.stopAnimate(),t.animate(e,300)):t.attr(e),t.setZIndex(0),t.set("_originAttrs",null))}})}t.get("children").sort(function(t,e){return t._INDEX-e._INDEX}),this.set("activeShapes",null),this.set("preHighlightShapes",null)}},getGroupShapesByPoint:function(t){var e=[];if(this.get("shapeContainer")){var n=this.getXScale().field,i=this.getShapes(),r=this._getOriginByPoint(t);a.each(i,function(t){var i=t.get("origin");if(t.get("visible")&&i){i._origin[n]===r[n]&&e.push(t)}})}return e},getSingleShapeByPoint:function(t){var e,n=this.get("shapeContainer"),i=n.get("canvas").get("pixelRatio");if(n&&(e=n.getShape(t.x*i,t.y*i)),e&&e.get("origin"))return e},highlightShapes:function(t,e){a.isArray(t)||(t=[t]);var n=this.get("activeShapes");if(i(n,t)){n&&this.clearActivedShapes();var o=this.getShapes(),s=this.get("activedOptions"),l=s&&s.animate,u=s&&s.style;a.each(o,function(n){var i={};n.stopAnimate(),-1!==a.indexOf(t,n)?(a.mix(i,u,e),n.setZIndex(1)):(a.mix(i,{fillOpacity:.3,opacity:.3}),n.setZIndex(0));var o=r(i,n);n.setSilent("_originAttrs",o),l?n.animate(i,300):n.attr(i)}),this.set("preHighlightShapes",t),this.set("activeShapes",t)}}};t.exports=o},function(t,e,n){function i(t,e){if(r.isNil(t)||r.isNil(e))return!1;var n=t.get("origin"),i=e.get("origin");return r.isEqual(n,i)}var r=n(0),a={_isAllowSelect:function(){var t=this.get("allowSelect");if(!r.isNil(t))return t;var e=this.get("type"),n=this.get("coord"),i=n&&n.type;return"interval"===e&&"theta"===i},_onClick:function(t){if(this._isAllowSelect()){this.clearActivedShapes();var e=t.shape,n=this.get("shapeContainer");e&&!e.get("animating")&&n.contain(e)&&this.setShapeSelected(e)}},_bindSelectedAction:function(){var t=this.get("view"),e=this.get("type");t.on(e+":click",r.wrapBehavior(this,"_onClick"))},_offSelectedAction:function(){var t=this.get("view"),e=this.get("type");t.off(e+":click",r.getWrapBehavior(this,"_onClick"))},_setShapeStatus:function(t,e){var n=this.get("view"),i=this.get("selectedOptions")||{},a=!1!==i.animate,o=n.get("canvas");t.set("selected",e);var s=t.get("origin");if(e){var l=s.shape||this.getDefaultValue("shape");r.isArray(l)&&(l=l[0]);var u=this.get("shapeFactory"),c=r.mix({geom:this,point:s},i),h=u.getSelectedCfg(l,c);r.mix(h,c.style),t.get("_originAttrs")||(t.get("animating")&&t.stopAnimate(),t.set("_originAttrs",function(t,e){var n={};return r.each(t,function(t,i){"transform"===i&&(i="matrix");var a=e.attr(i);r.isArray(a)&&(a=r.cloneDeep(a)),n[i]=a}),n}(h,t))),a?t.animate(h,300):(t.attr(h),o.draw())}else{var f=t.get("_originAttrs");t.set("_originAttrs",null),a?t.animate(f,300):(t.attr(f),o.draw())}},setShapeSelected:function(t){var e=this._getSelectedShapes(),n=this.get("selectedOptions")||{},a=!1!==n.cancelable;if("multiple"===n.mode)-1===r.indexOf(e,t)?(e.push(t),this._setShapeStatus(t,!0)):a&&(r.Array.remove(e,t),this._setShapeStatus(t,!1));else{var o=e[0];a&&(t=i(o,t)?null:t),i(o,t)||(o&&this._setShapeStatus(o,!1),t&&this._setShapeStatus(t,!0))}},clearSelected:function(){var t=this,e=t.get("shapeContainer");if(e&&!e.get("destroyed")){var n=t._getSelectedShapes();r.each(n,function(e){t._setShapeStatus(e,!1),e.set("_originAttrs",null)})}},setSelected:function(t){var e=this,n=e.getShapes();return r.each(n,function(n){var i=n.get("origin");i&&i._origin===t&&e.setShapeSelected(n)}),this},_getSelectedShapes:function(){var t=this.getShapes(),e=[];return r.each(t,function(t){t.get("selected")&&e.push(t)}),this.set("selectedShapes",e),e}};t.exports=a},function(t,e,n){var i=n(0);t.exports=function(t){return i.isArray(t)?t:i.isString(t)?t.split("*"):[t]}},function(t,e,n){var i=n(74),r=n(0),a=/^(?:(?!0000)[0-9]{4}([-/.]+)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\2(?:29))(\s+([01]|([01][0-9]|2[0-3])):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9]))?$/,o={LINEAR:"linear",CAT:"cat",TIME:"time"},s=function(){function t(t){this.defs={},this.viewTheme={scales:{}},this.filters={},r.assign(this,t)}var e=t.prototype;return e._getDef=function(t){var e=this.defs,n=this.viewTheme,i=null;return(n.scales[t]||e[t])&&(i=r.mix({},n.scales[t]),r.each(e[t],function(t,e){r.isNil(t)?delete i[e]:i[e]=t}),this.filters[t]&&(delete i.min,delete i.max)),i},e._getDefaultType=function(t,e){var n=o.LINEAR,i=r.Array.firstValue(e,t);return r.isArray(i)&&(i=i[0]),a.test(i)?n=o.TIME:r.isString(i)&&(n=o.CAT),n},e._getScaleCfg=function(t,e,n){var a={field:e},o=r.Array.values(n,e);if(a.values=o,!i.isCategory(t)&&"time"!==t){var s=r.Array.getRange(o);a.min=s.min,a.max=s.max,a.nice=!0}return"time"===t&&(a.nice=!1),a},e.createScale=function(t,e){var n,a=this._getDef(t);if(!e||!e.length)return n=a&&a.type?i[a.type](a):i.identity({value:t,field:t.toString(),values:[t]});var o=r.Array.firstValue(e,t);if(r.isNumber(t)||r.isNil(o)&&!a)n=i.identity({value:t,field:t.toString(),values:[t]});else{var s;a&&(s=a.type),s=s||this._getDefaultType(t,e);var l=this._getScaleCfg(s,t,e);a&&r.mix(l,a),n=i[s](l)}return n},t}();t.exports=s},function(t,e,n){var i=n(0),r=n(343),a=function(){function t(t){this.type="rect",this.actions=[],this.cfg={},i.mix(this,t),this.option=t||{}}var e=t.prototype;return e.reset=function(t){return this.actions=t.actions||[],this.type=t.type,this.cfg=t.cfg,this.option.actions=this.actions,this.option.type=this.type,this.option.cfg=this.cfg,this},e._execActions=function(t){var e=this.actions;i.each(e,function(e){var n=e[0];t[n](e[1],e[2])})},e.hasAction=function(t){var e=this.actions,n=!1;return i.each(e,function(e){if(t===e[0])return n=!0,!1}),n},e.createCoord=function(t,e){var n,a,o=this.type,s=this.cfg,l=i.mix({start:t,end:e},s);return"theta"===o?(n=r.Polar,this.hasAction("transpose")||this.transpose(),(a=new n(l)).type=o):a=new(n=r[i.upperFirst(o||"")]||r.Rect)(l),this._execActions(a),a},e.rotate=function(t){return t=t*Math.PI/180,this.actions.push(["rotate",t]),this},e.reflect=function(t){return this.actions.push(["reflect",t]),this},e.scale=function(t,e){return this.actions.push(["scale",t,e]),this},e.transpose=function(){return this.actions.push(["transpose"]),this},t}();t.exports=a},function(t,e,n){"use strict";var i=n(44);i.Cartesian=n(344),i.Rect=i.Cartesian,i.Polar=n(345),i.Helix=n(346),t.exports=i},function(t,e,n){"use strict";function i(t){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){return!e||"object"!==i(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function a(t,e){for(var n=0;nf/l?(a=f/l,o={x:n.x-(.5-c)*f,y:n.y-(.5-h)*a*u}):(a=p/u,o={x:n.x-(.5-c)*a*l,y:n.y-(.5-h)*p}),t?t>0&&t<=1?t*=a:(t<=0||t>a)&&(t=a):t=a;var g={start:i,end:r},d={start:e*t,end:t};this.x=g,this.y=d,this.radius=t,this.circleCentre=o,this.center=o}},{key:"getCenter",value:function(){return this.circleCentre}},{key:"getOneBox",value:function(){var t=this.startAngle,e=this.endAngle;if(Math.abs(e-t)>=2*Math.PI)return{minX:-1,maxX:1,minY:-1,maxY:1};for(var n=[0,Math.cos(t),Math.cos(e)],i=[0,Math.sin(t),Math.sin(e)],r=Math.min(t,e);r0?l:-l;var u=this.invertDim(s,"y"),c={};return c.x=this.isTransposed?u:l,c.y=this.isTransposed?l:u,c}}]),e}();t.exports=y},function(t,e,n){"use strict";function i(t){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){return!e||"object"!==i(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function a(t,e){for(var n=0;n=0&&n<=1&&(s*=n);var l=Math.floor(s*(1-i)/o),u=l/(2*Math.PI),c={start:r,end:a},h={start:i*s,end:i*s+.99*l};this.a=u,this.d=l,this.x=c,this.y=h}},{key:"getCenter",value:function(){return this.center}},{key:"convertPoint",value:function(t){var e,n,i=this.a,r=this.center;this.isTransposed?(e=t.y,n=t.x):(e=t.x,n=t.y);var a=this.convertDim(e,"x"),o=i*a,s=this.convertDim(n,"y");return{x:r.x+Math.cos(a)*(o+s),y:r.y+Math.sin(a)*(o+s)}}},{key:"invertPoint",value:function(t){var e=this.center,n=this.a,i=this.d+this.y.start,r=g.subtract([],[t.x,t.y],[e.x,e.y]),a=g.angleTo(r,[1,0],!0),o=a*n;g.length(r)u.x||!o&&s.y>u.y?1:-1,{isVertical:o,factor:r,start:s,end:l}},e._getCircleCfg=function(t){var e,n={},i=t.x,r=t.y,a=r.start>r.end;e=t.isTransposed?{x:a?0:1,y:0}:{x:0,y:a?0:1},e=t.convert(e);var s,l=t.circleCentre,u=[e.x-l.x,e.y-l.y],c=[1,0],h=(s=e.y>l.y?o.angle(u,c):-1*o.angle(u,c))+(i.end-i.start);return n.startAngle=s,n.endAngle=h,n.center=l,n.radius=Math.sqrt(Math.pow(e.x-l.x,2)+Math.pow(e.y-l.y,2)),n.inner=t.innerRadius||0,n},e._getRadiusCfg=function(t){var e,n,i=t.x.start<0?-1:1;return t.isTransposed?(e={x:0,y:0},n={x:1,y:0}):(e={x:0,y:0},n={x:0,y:1}),{factor:i,start:t.convert(e),end:t.convert(n)}},e._getAxisPosition=function(t,e,n,i){var r="",a=this.options;if(a[i]&&a[i].position)r=a[i].position;else{var o=t.type;t.isRect?"x"===e?r="bottom":"y"===e&&(r=n?"right":"left"):r="helix"===o?"helix":"x"===e?t.isTransposed?"radius":"circle":t.isTransposed?"circle":"radius"}return r},e._getAxisDefaultCfg=function(t,e,n,i){var a=this.viewTheme,o={},s=this.options,l=e.field;if(o=r.deepMix({},a.axis[i],o,s[l]),o.viewTheme=a,o.title){var u=r.isPlainObject(o.title)?o.title:{};u.text=u.text||e.alias||l,r.deepMix(o,{title:u})}return o.ticks=e.getTicks(),t.isPolar&&!e.isCategory&&"x"===n&&Math.abs(t.endAngle-t.startAngle)===2*Math.PI&&o.ticks.pop(),o.coord=t,o.label&&r.isNil(o.label.autoRotate)&&(o.label.autoRotate=!0),s.hasOwnProperty("xField")&&s.xField.hasOwnProperty("grid")&&"left"===o.position&&r.deepMix(o,s.xField),o},e._getAxisCfg=function(t,e,n,i,a,o){void 0===a&&(a="");var s=this,l=s._getAxisPosition(t,i,a,e.field),u=s._getAxisDefaultCfg(t,e,i,l);if(!r.isEmpty(u.grid)&&n){var c=[],h=[],f=function(t){var e=[];if(t.length>0){var n=(e=t.slice(0))[0],i=e[e.length-1];0!==n.value&&e.unshift({value:0}),1!==i.value&&e.push({value:1})}return e}(n.getTicks());if(f.length){var p=function(t,e,n){var i=[];return t.length<1?i:(t.length>=2&&e&&n&&i.push({text:"",tickValue:"",value:0}),0!==t[0].value&&i.push({text:"",tickValue:"",value:0}),1!==(i=i.concat(t))[i.length-1].value&&i.push({text:"",tickValue:"",value:1}),i)}(u.ticks,e.isLinear,"center"===u.grid.align);r.each(p,function(n,l){h.push(n.tickValue);var g=[],d=n.value;if("center"===u.grid.align&&(d=s._getMiddleValue(d,p,l,e.isLinear)),!r.isNil(d)){var v=t.x,y=t.y;r.each(f,function(e){var n="x"===i?d:e.value,r="x"===i?e.value:d,a=t.convert({x:n,y:r});if(t.isPolar){var o=t.circleCentre;y.start>y.end&&(r=1-r),a.flag=v.start>v.end?0:1,a.radius=Math.sqrt(Math.pow(a.x-o.x,2)+Math.pow(a.y-o.y,2))}g.push(a)}),c.push({_id:o+"-"+i+a+"-grid-"+n.tickValue,points:g})}})}u.grid.items=c,u.grid.tickValues=h}return u.type=e.type,u},e._getHelixCfg=function(t){for(var e={},n=t.a,i=t.startAngle,r=t.endAngle,a=[],o=0;o<=100;o++){var s=t.convert({x:o/100,y:0});a.push(s.x),a.push(s.y)}var l=t.convert({x:0,y:0});return e.a=n,e.startAngle=i,e.endAngle=r,e.crp=a,e.axisStart=l,e.center=t.center,e.inner=t.y.start,e},e._drawAxis=function(t,e,n,i,o,s,l){var u,c,h=this.container,f=this.canvas;"cartesian"===t.type?(u=a.Line,c=this._getLineCfg(t,e,i,l)):"helix"===t.type&&"x"===i?(u=a.Helix,c=this._getHelixCfg(t)):"x"===i?(u=a.Circle,c=this._getCircleCfg(t)):(u=a.Line,c=this._getRadiusCfg(t));var p=this._getAxisCfg(t,e,n,i,l,o);p=r.mix({},p,c),"y"===i&&s&&"circle"===s.get("type")&&(p.circle=s),p._id=o+"-"+i,r.isNil(l)||(p._id=o+"-"+i+l),r.mix(p,{canvas:f,group:h});var g=new u(p);return g.render(),this.axes.push(g),g},e.createAxis=function(t,e,n){var i=this,a=this.coord,o=a.type;if("theta"!==o&&("polar"!==o||!a.isTransposed)){var s;t&&!i._isHide(t.field)&&(s=i._drawAxis(a,t,e[0],"x",n)),r.isEmpty(e)||"helix"===o||r.each(e,function(e,r){i._isHide(e.field)||i._drawAxis(a,e,t,"y",n,s,r)})}},e.changeVisible=function(t){var e=this.axes;r.each(e,function(e){e.set("visible",t)})},e.clear=function(){var t=this.axes;r.each(t,function(t){t.clear()}),this.axes=[]},t}();t.exports=s},function(t,e,n){var i=n(0),r=n(349),a=function(){function t(t){this.guides=[],this.options=[],this.xScales=null,this.yScales=null,this.view=null,this.viewTheme=null,this.frontGroup=null,this.backGroup=null,i.mix(this,t)}var e=t.prototype;return e._creatGuides=function(){var t=this,e=this.options,n=this.xScales,a=this.yScales,o=this.view,s=this.viewTheme;return this.backContainer&&o&&(this.backGroup=this.backContainer.addGroup({viewId:o.get("_id")})),this.frontContainer&&o&&(this.frontGroup=this.frontContainer.addGroup({viewId:o.get("_id")})),e.forEach(function(e){var o=e.type,l=i.deepMix({xScales:n,yScales:a,viewTheme:s},s?s.guide[o]:{},e);o=i.upperFirst(o);var u=new r[o](l);t.guides.push(u)}),t.guides},e.line=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"line"},t)),this},e.arc=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"arc"},t)),this},e.text=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"text"},t)),this},e.image=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"image"},t)),this},e.region=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"region"},t)),this},e.regionFilter=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"regionFilter"},t)),this},e.dataMarker=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"dataMarker"},t)),this},e.dataRegion=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"dataRegion"},t)),this},e.html=function(t){return void 0===t&&(t={}),this.options.push(i.mix({type:"html"},t)),this},e.render=function(t){var e=this,n=e.view,r=n&&n.get("data"),a=e._creatGuides();i.each(a,function(i){var a;a=i.get("top")?e.frontGroup||e.frontContainer:e.backGroup||e.backContainer,i.render(t,a,r,n)})},e.clear=function(){this.options=[],this.reset()},e.changeVisible=function(t){var e=this.guides;i.each(e,function(e){e.changeVisible(t)})},e.reset=function(){var t=this.guides;i.each(t,function(t){t.clear()}),this.guides=[],this.backGroup&&this.backGroup.remove(),this.frontGroup&&this.frontGroup.remove()},t}();t.exports=a},function(t,e,n){var i=n(21).Guide,r=n(350);i.RegionFilter=r,t.exports=i},function(t,e,n){var i=n(0),r=n(15),a=n(25).Path,o=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{name:"regionFilter",zIndex:1,top:!0,start:null,end:null,color:null,apply:null,style:{opacity:1}})},n.render=function(t,e,n,i){var r=this,a=e.addGroup();a.name="guide-region-filter",i.once("afterpaint",function(){if(!a.get("destroyed")){r._drawShapes(i,a);var e=r._drawClip(t);a.attr({clip:e}),r.set("clip",e),r.get("appendInfo")&&a.setSilent("appendInfo",r.get("appendInfo")),r.set("el",a)}})},n._drawShapes=function(t,e){var n=this,r=[];return t.getAllGeoms().map(function(t){var a=t.getShapes(),o=t.get("type");return n._geomFilter(o)&&a.map(function(t){var a=t.type,o=i.cloneDeep(t.attr());n._adjustDisplay(o);var s=e.addShape(a,{attrs:o});return r.push(s),t}),t}),r},n._drawClip=function(t){var e=this.parsePoint(t,this.get("start")),n=this.parsePoint(t,this.get("end")),i=[["M",e.x,e.y],["L",n.x,e.y],["L",n.x,n.y],["L",e.x,n.y],["z"]];return new a({attrs:{path:i,opacity:1}})},n._adjustDisplay=function(t){var e=this.get("color");t.fill&&(t.fill=t.fillStyle=e),t.stroke=t.strokeStyle=e},n._geomFilter=function(t){var e=this.get("apply");return!e||i.contains(e,t)},n.clear=function(){t.prototype.clear.call(this);var e=this.get("clip");e&&e.remove()},e}(r);t.exports=o},function(t,e,n){var i=n(0),r=n(21).Legend,a=n(352),o=n(18),s=n(166),l=n(168),u=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,c=function(){function t(t){this.options={},i.mix(this,t),this.clear();var e=this.chart;this.container=e.get("frontPlot"),this.plotRange=e.get("plotRange")}var e=t.prototype;return e.clear=function(){var t=this.legends;this.backRange=null,i.each(t,function(t){i.each(t,function(t){t.destroy()})}),this.legends={}},e.getBackRange=function(){var t=this.backRange;if(!t){var e=this.chart.get("backPlot");t=s(e,l(this.chart.get("plotRange")));var n=this.plotRange;t.maxX-t.minX0){var a=e.getXScale(),o=e.getYScale(),s=a.field,l=o.field,u=t.get("origin")._origin,c=e.get("labelContainer").get("labelsGroup").get("children");i.each(c,function(e){var i=e.get("origin")||[];i[s]===u[s]&&i[l]===u[l]&&(e.set("visible",n),t.set("gLabel",e))})}}},e._bindFilterEvent=function(t,e){var n=this,r=this.chart,a=e.field;t.on("itemfilter",function(t){var e=t.range;r.filterShape(function(t,r,o){if(!i.isNil(t[a])){var s=t[a]>=e[0]&&t[a]<=e[1];return n._filterLabels(r,o,s),s}return!0});for(var o=r.getAllGeoms()||[],s=function(t){var n=o[t];"heatmap"===n.get("type")&&u(function(){n.drawWithRange(e)})},l=0;l1?l:n;if("left"===x[0]||"right"===x[0])s=u.br.y,m=this._getXAlign(x[0],o,n,c,g,d),_=e?e.get("group").get("y")+e.getHeight()+v:this._getYAlignVertical(x[1],s,b,c,0,d,a.get("height"));else if("top"===x[0]||"bottom"===x[0])if(_=this._getYAlignHorizontal(x[0],s,n,c,p,d),e){var w=e.getWidth();m=e.get("group").get("x")+w+v}else m=this._getXAlign(x[1],o,b,c,0,d),"right"===x[1]&&(m=u.br.x-b.totalWidth);t.move(m+h,_+f)},e._getXAlign=function(t,e,n,i,r,a){var o="left"===t?i.minX-r-a[3]:i.maxX+a[1];return"center"===t&&(o=(e-n.totalWidth)/2),o},e._getYAlignHorizontal=function(t,e,n,i,r,a){return"top"===t?i.minY-r-a[0]:i.maxY+a[2]},e._getYAlignVertical=function(t,e,n,i,r,a,o){var s="top"===t?i.minY-r-a[0]:e-n.totalHeight;return"center"===t&&(s=(o-n.totalHeight)/2),s},e._getSubRegion=function(t){var e=0,n=0,r=0,a=0;return i.each(t,function(t){var i=t.getWidth(),o=t.getHeight();e1){var g=Array(f.callback.length-1).fill("");c.color=f.mapping.apply(f,[l].concat(g)).join("")||b.defaultColor}else c.color=f.mapping(l).join("")||b.defaultColor;if(y&&p)if(p.callback&&p.callback.length>1){var v=Array(p.callback.length-1).fill("");m=p.mapping.apply(p,[l].concat(v)).join("")}else m=p.mapping(l).join("");var _=o.getShapeFactory(x).getMarkerCfg(m,c);i.isFunction(m)&&(_.symbol=m),d.push({value:r,dataValue:l,checked:h,marker:_})});var A=i.deepMix({},b.legend[M[0]],h[c]||h,{viewId:_.get("_id"),maxLength:C,items:d,container:g,position:[0,0]});A.title&&i.deepMix(A,{title:{text:t.alias||t.field}});var k;if(u._isTailLegend(h,n))A.chart=u.chart,A.geom=n,k=new a(A);else if(h.useHtml){var P=g.get("canvas").get("el");if(g=h.container,i.isString(g)&&/^\#/.test(g)){var T=g.replace("#","");g=document.getElementById(T)}g||(g=P.parentNode),A.container=g,void 0===A.legendStyle&&(A.legendStyle={}),A.legendStyle.CONTAINER_CLASS={position:"absolute",overflow:"auto","z-index":""===P.style.zIndex?1:parseInt(P.style.zIndex,10)+1},h.flipPage?(A.legendStyle.CONTAINER_CLASS.height="right"===M[0]||"left"===M[0]?C+"px":"auto",A.legendStyle.CONTAINER_CLASS.width="right"!==M[0]&&"left"!==M[0]?C+"px":"auto",k=new r.CatPageHtml(A)):k=new r.CatHtml(A)}else k=new r.Category(A);return u._bindClickEvent(k,t,s),p[l].push(k),k},e._bindChartMove=function(t){var e=this.chart,n=this.legends;e.on("plotmove",function(e){var r=!1;if(e.target){var a=e.target.get("origin");if(a){var o=a._origin||a[0]._origin,s=t.field;if(o){var l=o[s];i.each(n,function(t){i.each(t,function(t){r=!0,!t.destroyed&&t.activate(l)})})}}}r||i.each(n,function(t){i.each(t,function(t){!t.destroyed&&t.deactivate()})})})},e._addContinuousLegend=function(t,e,n){var a=this.legends;a[n]=a[n]||[];var o,s,l,u=this.container,c=t.field,h=t.getTicks(),f=[],p=this.viewTheme;i.each(h,function(n){var i=n.value,r=t.invert(i),a=e.mapping(r).join("");f.push({value:n.tickValue,attrValue:a,color:a,scaleValue:i}),0===i&&(s=!0),1===i&&(l=!0)}),s||f.push({value:t.min,attrValue:e.mapping(0).join(""),color:e.mapping(0).join(""),scaleValue:0}),l||f.push({value:t.max,attrValue:e.mapping(1).join(""),color:e.mapping(1).join(""),scaleValue:1});var g=this.options,d=n.split("-"),v=p.legend[d[0]];(g&&!1===g.slidable||g[c]&&!1===g[c].slidable)&&(v=i.mix({},v,p.legend.gradient));var y=i.deepMix({},v,g[c]||g,{items:f,attr:e,formatter:t.formatter,container:u,position:[0,0]});if(y.title&&i.deepMix(y,{title:{text:t.alias||t.field}}),"color"===e.type)o=new r.Color(y);else{if("size"!==e.type)return;o=g&&"circle"===g.sizeType?new r.CircleSize(y):new r.Size(y)}return this._bindFilterEvent(o,t),a[n].push(o),o},e._isTailLegend=function(t,e){if(t.hasOwnProperty("attachLast")&&t.attachLast){var n=e.get("type");if("line"===n||"lineStack"===n||"area"===n||"areaStack"===n)return!0}return!1},e._adjustPosition=function(t,e){var n;if(e)n="right-top";else if(i.isArray(t))n=String(t[0])+"-"+String(t[1]);else{var r=t.split("-");1===r.length?("left"===r[0]&&(n="left-bottom"),"right"===r[0]&&(n="right-bottom"),"top"===r[0]&&(n="top-center"),"bottom"===r[0]&&(n="bottom-center")):n=t}return n},e.addLegend=function(t,e,n,i){var r=this.options,a=t.field,o=r[a],s=this.viewTheme;if(!1===o)return null;if(o&&o.custom)this.addCustomLegend(a);else{var l=r.position||s.defaultLegendPosition;l=this._adjustPosition(l,this._isTailLegend(r,n)),o&&o.position&&(l=this._adjustPosition(o.position,this._isTailLegend(o,n)));var u;(u=t.isLinear?this._addContinuousLegend(t,e,l):this._addCategoryLegend(t,e,n,i,l))&&(this._bindHoverEvent(u,a),r.reactive&&this._bindChartMove(t))}},e.addCustomLegend=function(t){var e=this.chart,n=this.viewTheme,a=this.container,o=this.options;t&&(o=o[t]);var s=o.position||n.defaultLegendPosition;s=this._adjustPosition(s);var l=this.legends;l[s]=l[s]||[];var u=o.items;if(u){var c=e.getAllGeoms();i.each(u,function(t){var e=function(t,e){var n;return i.each(t,function(t){t.get("visible")&&t.getYScale().field===e&&(n=t)}),n}(c,t.value);i.isObject(t.marker)?t.marker.radius=t.marker.radius||4.5:t.marker={symbol:t.marker?t.marker:"circle",fill:t.fill,radius:4.5},t.checked=!!i.isNil(t.checked)||t.checked,t.geom=e});var h,f=e.get("canvas"),p=this.plotRange,g=s.split("-"),d="right"===g[0]||"left"===g[0]?p.bl.y-p.tr.y:f.get("width"),v=i.deepMix({},n.legend[g[0]],o,{maxLength:d,items:u,container:a,position:[0,0]});if(o.useHtml){var y=o.container;if(/^\#/.test(a)){var x=y.replace("#","");y=document.getElementById(x)}else y||(y=a.get("canvas").get("el").parentNode);v.container=y,void 0===v.legendStyle&&(v.legendStyle={}),v.legendStyle.CONTAINER_CLASS||(v.legendStyle.CONTAINER_CLASS={height:"right"===g[0]||"left"===g[0]?d+"px":"auto",width:"right"!==g[0]&&"left"!==g[0]?d+"px":"auto",position:"absolute",overflow:"auto"}),h=o.flipPage?new r.CatPageHtml(v):new r.CatHtml(v)}else h=new r.Category(v);return l[s].push(h),h.on("itemclick",function(t){o.onClick&&o.onClick(t)}),this._bindHoverEvent(h),h}},e.addMixedLegend=function(t,e){var n=[];i.each(t,function(t){var r=t.field;i.each(e,function(e){if(e.getYScale()===t&&t.values&&t.values.length>0){var i=e.get("shapeType")||"point",a=e.getDefaultValue("shape")||"circle",s=o.getShapeFactory(i),l={color:e.getDefaultValue("color")},u=s.getMarkerCfg(a,l),c={value:r,marker:u};n.push(c)}})});var r={custom:!0,items:n};this.options=i.deepMix({},r,this.options);var a=this.addCustomLegend();this._bindClickEventForMix(a)},e.alignLegends=function(){var t=this,e=t.legends,n=t._getRegion(e);t.totalRegion=n;var r=0;return i.each(e,function(e,a){var o=n.subs[r];i.each(e,function(n,i){var r=e[i-1];n.get("useHtml")&&!n.get("autoPosition")||t._alignLegend(n,r,o,a)}),r++}),this},t}();t.exports=c},function(t,e,n){var i=n(0),r=n(21),a=n(7),o=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{type:"tail-legend",layout:"vertical",autoLayout:!0})},n._addItem=function(t){var e=this.get("itemsGroup"),n=this._getNextX(),r=this.get("unCheckColor"),a=e.addGroup({x:0,y:0,value:t.value,scaleValue:t.scaleValue,checked:t.checked});a.translate(n,0),a.set("viewId",e.get("viewId"));var o=this.get("textStyle"),s=this.get("_wordSpaceing"),l=0;if(t.marker){var u=i.mix({},t.marker,{x:t.marker.radius,y:0});t.checked||(u.fill&&(u.fill=r),u.stroke&&(u.stroke=r));var c=a.addShape("marker",{type:"marker",attrs:u});c.attr("cursor","pointer"),c.name="legend-marker",l+=c.getBBox().width+s}var h=i.mix({},o,{x:l,y:0,text:this._formatItemValue(t.value)});t.checked||i.mix(h,{fill:r});var f=a.addShape("text",{attrs:h});f.attr("cursor","pointer"),f.name="legend-text",this.get("appendInfo")&&f.setSilent("appendInfo",this.get("appendInfo"));var p=a.getBBox(),g=this.get("itemWidth"),d=a.addShape("rect",{attrs:{x:n,y:0-p.height/2,fill:"#fff",fillOpacity:0,width:g||p.width,height:p.height}});return d.attr("cursor","pointer"),d.setSilent("origin",t),d.name="legend-item",this.get("appendInfo")&&d.setSilent("appendInfo",this.get("appendInfo")),a.name="legendGroup",a},n._adjust=function(){if(this.get("geom")){this.get("group").attr("matrix")[7]=0;var t=this.get("geom").get("dataArray"),e=this.get("itemsGroup").get("children"),n=0;i.each(e,function(e){var r=t[n],a=r[r.length-1].y;i.isArray(a)&&(a=a[1]);var o=e.getBBox().height,s=e.get("x"),l=a-o/2;e.translate(s,l),n++}),this.get("autoLayout")&&this._antiCollision(e)}},n.render=function(){var e=this;t.prototype.render.call(this);this.get("chart").once("afterpaint",function(){e._adjust()})},n._getPreviousY=function(t){return t.attr("matrix")[7]+t.getBBox().height},n._adjustDenote=function(t,e,n){var i=2*-a.legend.legendMargin;t.addShape("path",{attrs:{path:"M-2,"+e+"L"+i+","+(n+3),lineWidth:1,lineDash:[2,2],stroke:"#999999"}})},n._antiCollision=function(t){var e=this;t.sort(function(t,e){return t.attr("matrix")[7]-e.attr("matrix")[7]});var n=!0,i=e.get("chart").get("plotRange"),r=i.tl.y,a=Math.abs(r-i.bl.y),o=t[0].getBBox().height,s=Number.MIN_VALUE,l=0,u=t.map(function(t){var e=t.attr("matrix")[7];return e>l&&(l=e),e0){var g=u[c-1],d=u[c];g.pos+g.size>d.pos&&(g.size+=d.size,g.targets=g.targets.concat(d.targets),u.splice(c,1),n=!0)}}c=0;var v=this.get("itemsGroup").addGroup();u.forEach(function(n){var i=r+o;n.targets.forEach(function(){var r=t[c].attr("matrix")[7],a=n.pos+i-o/2;Math.abs(r-a)>o/2&&e._adjustDenote(v,a,r-e.get("group").attr("matrix")[7]/2),t[c].translate(0,-r),t[c].translate(0,a),i+=o,c++})})},e}(r.Legend.Category);t.exports=o},function(t,e,n){function i(t,e){if(!t)return!1;return!!t.className&&-1!==(a.isNil(t.className.baseVal)?t.className:t.className.baseVal).indexOf(e)}function r(t){var e=[];return a.each(t,function(t){var n=function(t,e){var n=-1;return a.each(t,function(t,i){var r=!0;for(var o in e)if(e.hasOwnProperty(o)&&-1===h.indexOf(o)&&!a.isObject(e[o])&&e[o]!==t[o]){r=!1;break}if(r)return n=i,!1}),n}(e,t);-1===n?e.push(t):e[n]=t}),e}var a=n(0),o=n(18),s=n(21).Tooltip,l=a.MatrixUtil.vec2,u=["line","area","path","areaStack"],c=["line","area"],h=["marker","showMarker"],f=function(){function t(t){a.assign(this,t),this.timeStamp=0}var e=t.prototype;return e._normalizeEvent=function(t){var e=this.chart,n=this._getCanvas(),i=n.getPointByClient(t.clientX,t.clientY),r=n.get("pixelRatio");i.x=i.x/r,i.y=i.y/r;var a=e.getViewsByPoint(i);return i.views=a,i},e._getCanvas=function(){return this.chart.get("canvas")},e._getTriggerEvent=function(){var t,e=this.options.triggerOn;return e&&"mousemove"!==e?"click"===e?t="plotclick":"none"===e&&(t=null):t="plotmove",t},e._getDefaultTooltipCfg=function(){var t=this.chart,e=this.viewTheme,n=this.options,i=a.mix({},e.tooltip),r=t.getAllGeoms().filter(function(t){return t.get("visible")}),o=[];a.each(r,function(t){var e=t.get("type"),n=t.get("adjusts"),i=!1;n&&a.each(n,function(t){if("symmetric"===t.type||"Symmetric"===t.type)return i=!0,!1}),-1!==a.indexOf(o,e)||i||o.push(e)});var s,l=!(!r.length||!r[0].get("coord"))&&r[0].get("coord").isTransposed;if(r.length&&r[0].get("coord")&&"cartesian"===r[0].get("coord").type)if("interval"===o[0]&&!1!==n.shared){var u=a.mix({},e.tooltipCrosshairsRect);u.isTransposed=l,s={zIndex:0,crosshairs:u}}else if(a.indexOf(c,o[0])>-1){var h=a.mix({},e.tooltipCrosshairsLine);h.isTransposed=l,s={crosshairs:h}}return a.mix(i,s,{})},e._bindEvent=function(){var t=this.chart,e=this._getTriggerEvent();e&&(t.on(e,a.wrapBehavior(this,"onMouseMove")),t.on("plotleave",a.wrapBehavior(this,"onMouseOut")))},e._offEvent=function(){var t=this.chart,e=this._getTriggerEvent();e&&(t.off(e,a.getWrapBehavior(this,"onMouseMove")),t.off("plotleave",a.getWrapBehavior(this,"onMouseOut")))},e._setTooltip=function(t,e,n,i){var o=this.tooltip,s=this.prePoint;if(!s||s.x!==t.x||s.y!==t.y){e=r(e),this.prePoint=t;var l=this.chart,u=this.viewTheme,c=a.isArray(t.x)?t.x[t.x.length-1]:t.x,h=a.isArray(t.y)?t.y[t.y.length-1]:t.y;o.get("visible")||l.emit("tooltip:show",{x:c,y:h,tooltip:o});var f=e[0],p=f.title||f.name;o.isContentChange(p,e)&&(l.emit("tooltip:change",{tooltip:o,x:c,y:h,items:e}),p=e[0].title||e[0].name,o.setContent(p,e),a.isEmpty(n)?(o.clearMarkers(),o.set("markerItems",[])):!0===this.options.hideMarkers?o.set("markerItems",n):o.setMarkers(n,u.tooltipMarker));i===this._getCanvas()&&"mini"===o.get("type")?o.hide():(o.setPosition(c,h,i),o.show())}},e.hideTooltip=function(){var t=this.tooltip,e=this.chart,n=this._getCanvas();this.prePoint=null,t.hide(),e.emit("tooltip:hide",{tooltip:t}),n.draw()},e.onMouseMove=function(t){if(!a.isEmpty(t.views)){var e=this.timeStamp,n=+new Date,i={x:t.x,y:t.y};n-e>16&&!this.chart.get("stopTooltip")&&(this.showTooltip(i,t.views,t.shape),this.timeStamp=n)}},e.onMouseOut=function(t){var e=this.tooltip;e.get("visible")&&e.get("follow")&&(t&&t.toElement&&(i(t.toElement,"g2-tooltip")||function(t,e){for(var n=t.parentNode,r=!1;n&&n!==document.body;){if(i(n,e)){r=!0;break}n=n.parentNode}return r}(t.toElement,"g2-tooltip"))||this.hideTooltip())},e.renderTooltip=function(){var t=this;if(!t.tooltip){var e=t.chart,n=t.viewTheme,i=t._getCanvas(),r=t._getDefaultTooltipCfg(),o=t.options;(o=a.deepMix({plotRange:e.get("plotRange"),capture:!1,canvas:i,frontPlot:e.get("frontPlot"),viewTheme:n.tooltip,backPlot:e.get("backPlot")},r,o)).crosshairs&&"rect"===o.crosshairs.type&&(o.zIndex=0),o.visible=!1;var l;"mini"===o.type?(o.crosshairs=!1,o.position="top",l=new s.Mini(o)):l=o.useHtml?new s.Html(o):new s.Canvas(o),t.tooltip=l;var u=t._getTriggerEvent();if(!l.get("enterable")&&"plotmove"===u){var c=l.get("container");c&&(c.onmousemove=function(n){var i=t._normalizeEvent(n);e.emit(u,i)})}t._bindEvent()}},e.showTooltip=function(t,e,n){var i=this;if(!a.isEmpty(e)&&t){this.tooltip||this.renderTooltip();var r=i.options,o=[],s=[];if(a.each(e,function(e){if(!e.get("tooltipEnable"))return!0;var n=e.get("geoms"),l=e.get("coord");a.each(n,function(e){var n=e.get("type");if(e.get("visible")&&!1!==e.get("tooltipCfg")){var c=e.get("dataArray");if(e.isShareTooltip()||!1===r.shared&&a.inArray(["area","line","path","polygon"],n))a.each(c,function(c){var h=e.findPoint(t,c);if(h){var f=e.getTipItems(h,r.title);a.each(f,function(t){var r=t.point;if(r&&r.x&&r.y){var s=a.isArray(r.x)?r.x[r.x.length-1]:r.x,c=a.isArray(r.y)?r.y[r.y.length-1]:r.y;r=l.applyMatrix(s,c,1),t.x=r[0],t.y=r[1],t.showMarker=!0;var h=i._getItemMarker(e,t.color);t.marker=h,-1!==a.indexOf(u,n)&&o.push(t)}}),s=s.concat(f)}});else{var h=e.get("shapeContainer"),f=h.get("canvas").get("pixelRatio"),p=h.getShape(t.x*f,t.y*f);p&&p.get("visible")&&p.get("origin")&&(s=e.getTipItems(p.get("origin"),r.title))}}}),a.each(s,function(t){var e=t.point,n=a.isArray(e.x)?e.x[e.x.length-1]:e.x,i=a.isArray(e.y)?e.y[e.y.length-1]:e.y;e=l.applyMatrix(n,i,1),t.x=e[0],t.y=e[1]})}),s.length){var c=s[0];if(!s.every(function(t){return t.title===c.title})){var h=c,f=1/0;s.forEach(function(e){var n=l.distance([t.x,t.y],[e.x,e.y]);n1){var p=s[0],g=Math.abs(t.y-p.y);a.each(s,function(e){Math.abs(t.y-e.y)<=g&&(p=e,g=Math.abs(t.y-e.y))}),p&&p.x&&p.y&&(o=[p]),s=[p]}i._setTooltip(t,s,o,n)}else i.hideTooltip()}},e.clear=function(){var t=this.tooltip;t&&t.destroy(),this.tooltip=null,this.prePoint=null,this._offEvent()},e._getItemMarker=function(t,e){var n=t.get("shapeType")||"point",i=t.getDefaultValue("shape")||"circle",r={color:e};return o.getShapeFactory(n).getMarkerCfg(i,r)},t}();t.exports=f},function(t,e,n){function i(t,e){if(a.isNil(t)||a.isNil(e))return!1;var n=t.get("origin"),i=e.get("origin");return a.isNil(n)&&a.isNil(i)?a.isEqual(t,e):a.isEqual(n,i)}function r(t){t.shape&&t.shape.get("origin")&&(t.data=t.shape.get("origin"))}var a=n(0),o=function(){function t(t){this.view=null,this.canvas=null,a.assign(this,t),this._init()}var e=t.prototype;return e._init=function(){this.pixelRatio=this.canvas.get("pixelRatio")},e._getShapeEventObj=function(t){return{x:t.x/this.pixelRatio,y:t.y/this.pixelRatio,target:t.target,toElement:t.event.toElement||t.event.relatedTarget}},e._getShape=function(t,e){return this.view.get("canvas").getShape(t,e)},e._getPointInfo=function(t){var e=this.view,n={x:t.x/this.pixelRatio,y:t.y/this.pixelRatio},i=e.getViewsByPoint(n);return n.views=i,n},e._getEventObj=function(t,e,n){return{x:e.x,y:e.y,target:t.target,toElement:t.event.toElement||t.event.relatedTarget,views:n}},e.bindEvents=function(){var t=this.canvas;t.on("mousedown",a.wrapBehavior(this,"onDown")),t.on("mousemove",a.wrapBehavior(this,"onMove")),t.on("mouseleave",a.wrapBehavior(this,"onOut")),t.on("mouseup",a.wrapBehavior(this,"onUp")),t.on("click",a.wrapBehavior(this,"onClick")),t.on("dblclick",a.wrapBehavior(this,"onClick")),t.on("touchstart",a.wrapBehavior(this,"onTouchstart")),t.on("touchmove",a.wrapBehavior(this,"onTouchmove")),t.on("touchend",a.wrapBehavior(this,"onTouchend"))},e._triggerShapeEvent=function(t,e,n){if(t&&t.name&&!t.get("destroyed")){var i=this.view;if(i.isShapeInView(t)){var r=t.name+":"+e;n.view=i,n.appendInfo=t.get("appendInfo"),i.emit(r,n);var a=i.get("parent");a&&a.emit(r,n)}}},e.onDown=function(t){var e=this.view,n=this._getShapeEventObj(t);n.shape=this.currentShape,r(n),e.emit("mousedown",n),this._triggerShapeEvent(this.currentShape,"mousedown",n)},e.onMove=function(t){var e=this.view,n=this.currentShape;n&&n.get("destroyed")&&(n=null,this.currentShape=null);var a=this._getShape(t.x,t.y)||t.currentTarget,o=this._getShapeEventObj(t);if(o.shape=a,r(o),e.emit("mousemove",o),this._triggerShapeEvent(a,"mousemove",o),n&&!i(n,a)){var s=this._getShapeEventObj(t);s.shape=n,s.toShape=a,r(s),this._triggerShapeEvent(n,"mouseleave",s)}if(a&&!i(n,a)){var l=this._getShapeEventObj(t);l.shape=a,l.fromShape=n,r(l),this._triggerShapeEvent(a,"mouseenter",l)}this.currentShape=a;var u=this._getPointInfo(t),c=this.curViews||[];0===c.length&&u.views.length&&e.emit("plotenter",this._getEventObj(t,u,u.views)),c.length&&0===u.views.length&&e.emit("plotleave",this._getEventObj(t,u,c)),u.views.length&&((o=this._getEventObj(t,u,u.views)).shape=a,r(o),e.emit("plotmove",o)),this.curViews=u.views},e.onOut=function(t){var e=this.view,n=this._getPointInfo(t),i=this.curViews||[],r=this._getEventObj(t,n,i);!this.curViews||0===this.curViews.length||r.toElement&&"CANVAS"===r.toElement.tagName||(e.emit("plotleave",r),this.curViews=[])},e.onUp=function(t){var e=this.view,n=this._getShapeEventObj(t);n.shape=this.currentShape,e.emit("mouseup",n),this._triggerShapeEvent(this.currentShape,"mouseup",n)},e.onClick=function(t){var e=this.view,n=this._getShape(t.x,t.y)||t.currentTarget,i=this._getShapeEventObj(t);i.shape=n,r(i),e.emit("click",i),this._triggerShapeEvent(n,t.type,i),this.currentShape=n;var o=this._getPointInfo(t),s=o.views;if(!a.isEmpty(s)){var l=this._getEventObj(t,o,s);if(this.currentShape){var u=this.currentShape;l.shape=u,r(l)}e.emit("plotclick",l),"dblclick"===t.type&&(e.emit("plotdblclick",l),e.emit("dblclick",i))}},e.onTouchstart=function(t){var e=this.view,n=this._getShape(t.x,t.y)||t.currentTarget,i=this._getShapeEventObj(t);i.shape=n,r(i),e.emit("touchstart",i),this._triggerShapeEvent(n,"touchstart",i),this.currentShape=n},e.onTouchmove=function(t){var e=this.view,n=this._getShape(t.x,t.y)||t.currentTarget,i=this._getShapeEventObj(t);i.shape=n,r(i),e.emit("touchmove",i),this._triggerShapeEvent(n,"touchmove",i),this.currentShape=n},e.onTouchend=function(t){var e=this.view,n=this._getShapeEventObj(t);n.shape=this.currentShape,r(n),e.emit("touchend",n),this._triggerShapeEvent(this.currentShape,"touchend",n)},e.clearEvents=function(){var t=this.canvas;t.off("mousemove",a.getWrapBehavior(this,"onMove")),t.off("mouseleave",a.getWrapBehavior(this,"onOut")),t.off("mousedown",a.getWrapBehavior(this,"onDown")),t.off("mouseup",a.getWrapBehavior(this,"onUp")),t.off("click",a.getWrapBehavior(this,"onClick")),t.off("dblclick",a.getWrapBehavior(this,"onClick")),t.off("touchstart",a.getWrapBehavior(this,"onTouchstart")),t.off("touchmove",a.getWrapBehavior(this,"onTouchmove")),t.off("touchend",a.getWrapBehavior(this,"onTouchend"))},t}();t.exports=o},function(t,e,n){function i(t,e){var n=[];if(!1===t.get("animate"))return[];var r=t.get("children");return s.each(r,function(t){if(t.isGroup)n=n.concat(i(t,e));else if(t.isShape&&t._id){var r=t._id;(r=r.split("-")[0])===e&&n.push(t)}}),n}function r(t,e,n,i){return i?l.Action[n][i]:l.getAnimation(t,e,n)}function a(t,e,n){var i=l.getAnimateCfg(t,e);return n&&n[e]?s.deepMix({},i,n[e]):i}function o(t,e,n,i){var o,l,c=!1;if(i){var h=[],f=[];s.each(e,function(e){var n=t[e._id];n?(e.setSilent("cacheShape",n),h.push(e),delete t[e._id]):f.push(e)}),s.each(t,function(t){var e=t.name,i=t.coord,h=t._id,f=t.attrs,p=t.index,g=t.type;if(l=a(e,"leave",t.animateCfg),o=r(e,i,"leave",l.animation),s.isFunction(o)){var d=n.addShape(g,{attrs:f,index:p});if(d._id=h,d.name=e,i&&"label"!==e){var v=d.getMatrix(),y=u.multiply([],v,i.matrix);d.setMatrix(y)}c=!0,o(d,l,i)}}),s.each(h,function(t){var e=t.name,n=t.get("coord"),i=t.get("cacheShape").attrs;if(!s.isEqual(i,t.attr())){if(l=a(e,"update",t.get("animateCfg")),o=r(e,n,"update",l.animation),s.isFunction(o))o(t,l,n);else{var u=s.cloneDeep(t.attr());t.attr(i),t.animate(u,l.duration,l.easing,function(){t.setSilent("cacheShape",null)})}c=!0}}),s.each(f,function(t){var e=t.name,n=t.get("coord");l=a(e,"enter",t.get("animateCfg")),o=r(e,n,"enter",l.animation),s.isFunction(o)&&(o(t,l,n),c=!0)})}else s.each(e,function(t){var e=t.name,n=t.get("coord");l=a(e,"appear",t.get("animateCfg")),o=r(e,n,"appear",l.animation),s.isFunction(o)&&(o(t,l,n),c=!0)});return c}var s=n(0),l=n(126),u=s.MatrixUtil.mat3;t.exports={execAnimation:function(t,e){var n=t.get("middlePlot"),r=t.get("backPlot"),a=t.get("_id"),l=t.get("canvas"),u=l.get(a+"caches")||[];0===u.length&&(e=!1);var c=i(n,a),h=i(r,a),f=c.concat(h);l.setSilent(a+"caches",function(t){var e={};return s.each(t,function(t){if(t._id&&!t.isClip){var n=t._id;e[n]={_id:n,type:t.get("type"),attrs:s.cloneDeep(t.attr()),name:t.name,index:t.get("index"),animateCfg:t.get("animateCfg"),coord:t.get("coord")}}}),e}(f));(e?o(u,f,l,e):o(u,c,l,e))||l.draw()}}},function(t,e,n){var i=n(0),r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){return{type:"plotBack",padding:null,background:null,plotRange:null,plotBackground:null}},n._beforeRenderUI=function(){this._calculateRange()},n._renderUI=function(){this._renderBackground(),this._renderPlotBackground()},n._renderBackground=function(){var t=this.get("background");if(t){var e=this.get("canvas"),n={x:0,y:0,width:this.get("width")||e.get("width"),height:this.get("height")||e.get("height")},r=this.get("backgroundShape");r?r.attr(n):(r=this.addShape("rect",{attrs:i.mix(n,t)}),this.set("backgroundShape",r))}},n._renderPlotBackground=function(){var t=this.get("plotBackground");if(t){var e=this.get("plotRange"),n=e.br.x-e.bl.x,r=e.br.y-e.tr.y,a=e.tl,o={x:a.x,y:a.y,width:n,height:r},s=this.get("plotBackShape");s?s.attr(o):(t.image?(o.img=t.image,s=this.addShape("image",{attrs:o})):(i.mix(o,t),s=this.addShape("rect",{attrs:o})),this.set("plotBackShape",s))}},n._convert=function(t,e){if(i.isString(t))if("auto"===t)t=0;else if(-1!==t.indexOf("%")){var n=this.get("canvas"),r=this.get("width")||n.get("width"),a=this.get("height")||n.get("height");t=parseInt(t,10)/100,t=e?t*r:t*a}return t},n._calculateRange=function(){var t=this.get("plotRange");i.isNil(t)&&(t={});var e=this.get("padding"),n=this.get("canvas"),r=this.get("width")||n.get("width"),a=this.get("height")||n.get("height"),o=i.toAllPadding(e),s=this._convert(o[0],!1),l=this._convert(o[1],!0),u=this._convert(o[2],!1),c=this._convert(o[3],!0),h=Math.min(c,r-l),f=Math.max(c,r-l),p=Math.min(a-u,s),g=Math.max(a-u,s);t.tl={x:h,y:p},t.tr={x:f,y:p},t.bl={x:h,y:g},t.br={x:f,y:g},t.cc={x:(f+h)/2,y:(g+p)/2},this.set("plotRange",t)},n.repaint=function(){return this._calculateRange(),this._renderBackground(),this._renderPlotBackground(),this},e}(n(16).Group);t.exports=r},function(t,e,n){var i=n(7),r=n(0),a={getDefaultSize:function(){var t=this.get("defaultSize"),e=this.get("viewTheme")||i;if(!t){var n,a=this.get("coord"),o=this.getXScale(),s=o.values,l=this.get("dataArray");if(o.isLinear&&s.length>1){s.sort();var u=function(t,e){var n=t.length;r.isString(t[0])&&(t=t.map(function(t){return e.translate(t)}));for(var i=t[1]-t[0],a=2;ao&&(i=o)}return i}(s,o);n=(o.max-o.min)/u,s.length>n&&(n=s.length)}else n=s.length;var c=o.range,h=1/n,f=1;if(this.isInCircle()?f=a.isTransposed&&n>1?e.widthRatio.multiplePie:e.widthRatio.rose:(o.isLinear&&(h*=c[1]-c[0]),f=e.widthRatio.column),h*=f,this.hasAdjust("dodge")){h/=this._getDodgeCount(l)}t=h,this.set("defaultSize",t)}return t},_getDodgeCount:function(t){var e,n=this.get("adjusts"),i=t.length;if(r.each(n,function(t){"dodge"===t.type&&(e=t.dodgeBy)}),e){var a=r.Array.merge(t);i=r.Array.values(a,e).length}return i},getDimWidth:function(t){var e=this.get("coord"),n=e.convertPoint({x:0,y:0}),i=e.convertPoint({x:"x"===t?1:0,y:"x"===t?0:1}),r=0;return n&&i&&(r=Math.sqrt(Math.pow(i.x-n.x,2)+Math.pow(i.y-n.y,2))),r},_getWidth:function(){var t=this.get("coord");return this.isInCircle()&&!t.isTransposed?(t.endAngle-t.startAngle)*t.radius:this.getDimWidth("x")},_toNormalizedSize:function(t){return t/this._getWidth()},_toCoordSize:function(t){return this._getWidth()*t},getNormalizedSize:function(t){var e=this.getAttrValue("size",t);return e=r.isNil(e)?this.getDefaultSize():this._toNormalizedSize(e)},getSize:function(t){var e=this.getAttrValue("size",t);if(r.isNil(e)){var n=this.getDefaultSize();e=this._toCoordSize(n)}return e}};t.exports=a},function(t,e,n){var i=n(0),r=n(7);t.exports={splitData:function(t){var e=this.get("viewTheme")||r;if(!t.length)return[];var n,a=[],o=[],s=this.getYScale().field;return i.each(t,function(t){n=t._origin?t._origin[s]:t[s],e.connectNulls?i.isNil(n)||o.push(t):i.isArray(n)&&i.isNil(n[0])||i.isNil(n)?o.length&&(a.push(o),o=[]):o.push(t)}),o.length&&a.push(o),a}}},function(t,e,n){function i(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var r=n(20),a=n(358),o=n(0),s=function(t){function e(e){var n;return n=t.call(this,e)||this,o.assign(i(i(n)),a),n}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return e.type="path",e.shapeType="line",e},n.getDrawCfg=function(e){var n=t.prototype.getDrawCfg.call(this,e);return n.isStack=this.hasStack(),n},n.draw=function(t,e,n,i){var r=this,a=this.splitData(t),s=this.getDrawCfg(t[0]);r._applyViewThemeShapeStyle(s,s.shape,n),s.origin=t,o.each(a,function(t,a){if(!o.isEmpty(t)){s.splitedIndex=a,s.points=t;var l=n.drawShape(s.shape,s,e);r.appendShapeInfo(l,i+a)}})},e}(r);r.Path=s,t.exports=s},function(t,e,n){"use strict";var i=n(370),r=n(371);e.a=function(t){var e=Object(i.a)(t);return(e.local?function(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}:function(t){return function(){var e=this.ownerDocument,n=this.namespaceURI;return n===r.b&&e.documentElement.namespaceURI===r.b?e.createElement(t):e.createElementNS(n,t)}})(e)}},function(t,e,n){"use strict";e.a=function(t,e){var n=t.ownerSVGElement||t;if(n.createSVGPoint){var i=n.createSVGPoint();return i.x=e.clientX,i.y=e.clientY,i=i.matrixTransform(t.getScreenCTM().inverse()),[i.x,i.y]}var r=t.getBoundingClientRect();return[e.clientX-r.left-t.clientLeft,e.clientY-r.top-t.clientTop]}},function(t,e,n){"use strict";e.b=function(t,e,n){var r=t._id;return t.each(function(){var t=Object(i.h)(this,r);(t.value||(t.value={}))[e]=n.apply(this,arguments)}),function(t){return Object(i.f)(t,r).value[e]}};var i=n(70);e.a=function(t,e){var n=this._id;if(t+="",arguments.length<2){for(var r,a=Object(i.f)(this.node(),n).tween,o=0,s=a.length;o0;)i-=2*Math.PI;var c=a-t+(i=i/Math.PI/2*n)-2*t;l.push(["M",c,e]);for(var h=0,f=0;f=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),i.a.hasOwnProperty(e)?{space:i.a[e],local:t}:t}},function(t,e,n){"use strict";n.d(e,"b",function(){return i});var i="http://www.w3.org/1999/xhtml";e.a={svg:"http://www.w3.org/2000/svg",xhtml:i,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"}},function(t,e,n){"use strict";e.a=function(t){return null==t?function(){}:function(){return this.querySelector(t)}}},function(t,e,n){"use strict";e.a=function(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}},function(t,e,n){"use strict";function i(t,e,n){return function(i){var r=o;o=i;try{t.call(this,this.__data__,e,n)}finally{o=r}}}function r(t,e,n){var r=a.hasOwnProperty(t.type)?function(t,e,n){return t=i(t,e,n),function(e){var n=e.relatedTarget;n&&(n===this||8&n.compareDocumentPosition(this))||t.call(this,e)}}:i;return function(i,a,o){var s,l=this.__on,u=r(e,a,o);if(l)for(var c=0,h=l.length;c=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}})}(t+""),s=o.length;{if(!(arguments.length<2)){for(l=e?r:function(t){return function(){var e=this.__on;if(e){for(var n,i=0,r=-1,a=e.length;in.max&&(n.max=e.max)):"timeCat"===o?(i.each(s,function(t,e){s[e]=r.toTimeStamp(t)}),s.sort(function(t,e){return t-e}),n=s):n=s,n}},function(t,e,n){"use strict";var i=n(69);e.a=function(t){return"string"==typeof t?new i.a([[document.querySelector(t)]],[document.documentElement]):new i.a([[t]],i.c)}},function(t,e,n){"use strict";e.a=function(t){return null==t?function(){return[]}:function(){return this.querySelectorAll(t)}}},function(t,e,n){"use strict";var i=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var r=document.documentElement;if(!r.matches){var a=r.webkitMatchesSelector||r.msMatchesSelector||r.mozMatchesSelector||r.oMatchesSelector;i=function(t){return function(){return a.call(this,t)}}}}e.a=i},function(t,e,n){"use strict";function i(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}e.a=i;var r=n(383),a=n(69);e.b=function(){return new a.a(this._enter||this._groups.map(r.a),this._parents)},i.prototype={constructor:i,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}}},function(t,e,n){"use strict";e.a=function(t){return new Array(t.length)}},function(t,e,n){"use strict";function i(t,e){return t.style.getPropertyValue(e)||Object(r.a)(t).getComputedStyle(t,null).getPropertyValue(e)}e.b=i;var r=n(373);e.a=function(t,e,n){return arguments.length>1?this.each((null==e?function(t){return function(){this.style.removeProperty(t)}}:"function"==typeof e?function(t,e,n){return function(){var i=e.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,n)}}:function(t,e,n){return function(){this.style.setProperty(t,e,n)}})(t,e,null==n?"":n)):i(this.node(),t)}},function(t,e,n){"use strict";var i=n(70);e.a=function(t,e){var n,r,a,o=t.__transition,s=!0;if(o){e=null==e?null:e+"";for(a in o)(n=o[a]).name===e?(r=n.state>i.d&&n.state0&&(o[0][0]="L"),i=i.concat(o)}),i.push(["Z"]),i}function o(t){return{symbol:function(t,e){return[["M",t-5.5,e-4],["L",t+5.5,e-4],["L",t+5.5,e+4],["L",t-5.5,e+4],["Z"]]},radius:5,fill:t.color,fillOpacity:.6}}var s=n(0),l=n(18),u=n(22),c=n(45),h=n(7),f=l.registerFactory("area",{defaultShapeType:"area",getDefaultPoints:function(t){var e=[],n=t.x,i=t.y,r=t.y0;return i=s.isArray(i)?i:[r,i],s.each(i,function(t){e.push({x:n,y:t})}),e},getActiveCfg:function(t,e){return function(t,e){if("line"===t||"smoothLine"===t)return{lineWidth:(e.lineWidth||0)+1};var n=e.fillOpacity||e.opacity||1;return{fillOpacity:n-.15,strokeOpacity:n-.15}}(t,e)},drawShape:function(t,e,n){var i,r=this.getShape(t);return(i=1===e.points.length&&h.showSinglePoint?function(t,e,n){var i=t._coord.convertPoint(e.points[0][1]);return n.addShape("circle",{attrs:s.mix({x:i.x,y:i.y,r:2,fill:e.color},e.style)})}(this,e,n):r.draw(e,n))&&(i.set("origin",e.origin),i._id=e.splitedIndex?e._id+e.splitedIndex:e._id,i.name=this.name),i},getSelectedCfg:function(t,e){return e&&e.style?e.style:this.getActiveCfg(t,e)}});l.registerShape("area","area",{draw:function(t,e){var n=r(t),i=a(t,!1,this);return e.addShape("path",{attrs:s.mix(n,{path:i})})},getMarkerCfg:function(t){return o(t)}}),l.registerShape("area","smooth",{draw:function(t,e){var n=r(t),i=this._coord;t.constraint=[[i.start.x,i.end.y],[i.end.x,i.start.y]];var o=a(t,!0,this);return e.addShape("path",{attrs:s.mix(n,{path:o})})},getMarkerCfg:function(t){return o(t)}}),l.registerShape("area","line",{draw:function(t,e){var n=i(t),r=a(t,!1,this);return e.addShape("path",{attrs:s.mix(n,{path:r})})},getMarkerCfg:function(t){return o(t)}}),l.registerShape("area","smoothLine",{draw:function(t,e){var n=i(t),r=a(t,!0,this);return e.addShape("path",{attrs:s.mix(n,{path:r})})},getMarkerCfg:function(t){return o(t)}}),f.spline=f.smooth,t.exports=f},function(t,e,n){var i=n(20);n(393);var r=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);return e.prototype.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return e.type="edge",e.shapeType="edge",e.generatePoints=!0,e},e}(i);i.Edge=r,t.exports=r},function(t,e,n){function i(t){var e=u.shape.edge,n=o.mix({},e,t.style);return l.addStrokeAttrs(n,t),n}function r(t,e){var n=[];n.push({x:t.x,y:.5*t.y+1*e.y/2}),n.push({y:.5*t.y+1*e.y/2,x:e.x}),n.push(e);var i=["C"];return o.each(n,function(t){i.push(t.x,t.y)}),i}function a(t,e){var n=[];n.push({x:e.x,y:e.y}),n.push(t);var i=["Q"];return o.each(n,function(t){i.push(t.x,t.y)}),i}var o=n(0),s=n(18),l=n(45),u=n(7),c=n(22),h=1/3,f=s.registerFactory("edge",{defaultShapeType:"line",getDefaultPoints:function(t){return l.splitPoints(t)},getActiveCfg:function(t,e){return{lineWidth:(e.lineWidth||0)+1}}});s.registerShape("edge","line",{draw:function(t,e){var n=this.parsePoints(t.points),r=i(t),a=c.getLinePath(n);return e.addShape("path",{attrs:o.mix(r,{path:a})})},getMarkerCfg:function(t){return o.mix({symbol:"circle",radius:4.5},i(t))}}),s.registerShape("edge","vhv",{draw:function(t,e){var n=t.points,r=i(t),a=function(t,e){var n=[];n.push({y:t.y*(1-h)+e.y*h,x:t.x}),n.push({y:t.y*(1-h)+e.y*h,x:e.x}),n.push(e);var i=[["M",t.x,t.y]];return o.each(n,function(t){i.push(["L",t.x,t.y])}),i}(n[0],n[1]);a=this.parsePath(a);return e.addShape("path",{attrs:o.mix(r,{path:a})})},getMarkerCfg:function(t){return o.mix({symbol:"circle",radius:4.5},i(t))}}),s.registerShape("edge","smooth",{draw:function(t,e){var n=t.points,a=i(t),s=function(t,e){var n=r(t,e),i=[["M",t.x,t.y]];return i.push(n),i}(n[0],n[1]);s=this.parsePath(s);return e.addShape("path",{attrs:o.mix(a,{path:s})})},getMarkerCfg:function(t){return o.mix({symbol:"circle",radius:4.5},i(t))}}),s.registerShape("edge","arc",{draw:function(t,e){var n,s,l=t.points,u=l.length>2?"weight":"normal",c=i(t);if(t.isInCircle){var h={x:0,y:1};"normal"===u?s=function(t,e,n){var i=a(e,n),r=[["M",t.x,t.y]];return r.push(i),r}(l[0],l[1],h):(c.fill=c.stroke,s=function(t,e){var n=a(t[1],e),i=a(t[3],e),r=[["M",t[0].x,t[0].y]];return r.push(i),r.push(["L",t[3].x,t[3].y]),r.push(["L",t[2].x,t[2].y]),r.push(n),r.push(["L",t[1].x,t[1].y]),r.push(["L",t[0].x,t[0].y]),r.push(["Z"]),r}(l,h)),s=this.parsePath(s),n=e.addShape("path",{attrs:o.mix(c,{path:s})})}else if("normal"===u)l=this.parsePoints(l),n=e.addShape("arc",{attrs:o.mix(c,{x:(l[1].x+l[0].x)/2,y:l[0].y,r:Math.abs(l[1].x-l[0].x)/2,startAngle:Math.PI,endAngle:2*Math.PI})});else{s=[["M",l[0].x,l[0].y],["L",l[1].x,l[1].y]];var f=r(l[1],l[3]),p=r(l[2],l[0]);s.push(f),s.push(["L",l[3].x,l[3].y]),s.push(["L",l[2].x,l[2].y]),s.push(p),s.push(["Z"]),s=this.parsePath(s),c.fill=c.stroke,n=e.addShape("path",{attrs:o.mix(c,{path:s})})}return n},getMarkerCfg:function(t){return o.mix({symbol:"circle",radius:4.5},i(t))}}),t.exports=f},function(t,e,n){var i=n(73).ColorUtil,r=n(20),a=n(0),o=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return e.type="heatmap",e.paletteCache={},e},n._prepareRange=function(){var t=this.get("mappedData"),e=this.getAttr("color").field,n=1/0,i=-1/0;t.forEach(function(t){var r=t._origin[e];r>i&&(i=r),r=t[0]}));for(var c=this._getScale(o),h=0;h1?t[1]:e;return{min:e,max:n,min1:i,max1:t.length>3?t[3]:n,median:t.length>2?t[2]:i}}function r(t,e,n){var r,a,s=[];return o.isArray(e)?r=[[t-n/2,(a=i(e)).max],[t+n/2,a.max],[t,a.max],[t,a.max1],[t-n/2,a.min1],[t-n/2,a.max1],[t+n/2,a.max1],[t+n/2,a.min1],[t,a.min1],[t,a.min],[t-n/2,a.min],[t+n/2,a.min],[t-n/2,a.median],[t+n/2,a.median]]:(e=e||.5,r=[[(a=i(t)).min,e-n/2],[a.min,e+n/2],[a.min,e],[a.min1,e],[a.min1,e-n/2],[a.min1,e+n/2],[a.max1,e+n/2],[a.max1,e-n/2],[a.max1,e],[a.max,e],[a.max,e-n/2],[a.max,e+n/2],[a.median,e-n/2],[a.median,e+n/2]]),function(t,e){o.each(t,function(t){e.push({x:t[0],y:t[1]})})}(r,s),s}function a(t,e,n){var i=function(t){o.isArray(t)||(t=[t]);var e=t.sort(function(t,e){return te[n].radius+B)return!1;return!0}(e,t)}),u=0,c=0,h=[];if(o.length>1){var f=l(o);for(n=0;n-1){var m=t[d.parentIndex[x]],_=Math.atan2(d.x-m.x,d.y-m.y),b=Math.atan2(g.x-m.x,g.y-m.y),w=b-_;w<0&&(w+=2*Math.PI);var S=b-w/2,M=a(v,{x:m.x+m.radius*Math.sin(S),y:m.y+m.radius*Math.cos(S)});M>2*m.radius&&(M=2*m.radius),(null===y||y.width>M)&&(y={circle:m,width:M,p1:d,p2:g})}null!==y&&(h.push(y),u+=r(y.circle.radius,y.width),g=d)}}else{var C=t[0];for(n=1;nMath.abs(C.radius-t[n].radius)){A=!0;break}A?u=c=0:(u=C.radius*C.radius*Math.PI,h.push({circle:C,p1:{x:C.x,y:C.y+C.radius},p2:{x:C.x-B,y:C.y+C.radius},width:2*C.radius}))}return c/=2,e&&(e.area=u+c,e.arcArea=u,e.polygonArea=c,e.arcs=h,e.innerPoints=o,e.intersectionPoints=i),u+c}function r(t,e){return t*t*Math.acos(1-e/t)-(t-e)*Math.sqrt(e*(2*t-e))}function a(t,e){return Math.sqrt((t.x-e.x)*(t.x-e.x)+(t.y-e.y)*(t.y-e.y))}function o(t,e,n){if(n>=t+e)return 0;if(n<=Math.abs(t-e))return Math.PI*Math.min(t,e)*Math.min(t,e);var i=e-(n*n-t*t+e*e)/(2*n);return r(t,t-(n*n-e*e+t*t)/(2*n))+r(e,i)}function s(t,e){var n=a(t,e),i=t.radius,r=e.radius;if(n>=i+r||n<=Math.abs(i-r))return[];var o=(i*i-r*r+n*n)/(2*n),s=Math.sqrt(i*i-o*o),l=t.x+o*(e.x-t.x)/n,u=t.y+o*(e.y-t.y)/n,c=-(e.y-t.y)*(s/n),h=-(e.x-t.x)*(s/n);return[{x:l+c,y:u-h},{x:l-c,y:u+h}]}function l(t){for(var e={x:0,y:0},n=0;n=v[d-1].fx){var P=!1;if(b.fx>k.fx?(g(w,1+f,_,-f,k),w.fx=t(w),w.fx=1)break;for(y=1;yl+a*r*u||c>=d)f=r;else{if(Math.abs(p)<=-o*u)return r;p*(f-s)>=0&&(f=s),s=r,d=c}return 0}var l=n.fx,u=h(n.fxprime,e),c=l,f=l,p=u,d=0;r=r||1,a=a||1e-6,o=o||.1;for(var v=0;v<10;++v){if(g(i.x,1,n.x,r,e),c=i.fx=t(i.x,i.fxprime),p=h(i.fxprime,e),c>l+a*r*u||v&&c>=f)return s(d,r,f);if(Math.abs(p)<=-o*u)return r;if(p>=0)return s(r,d,c);f=c,d=r,r*=2}return r}function y(t,e,n){var i,r,a,o={x:e.slice(),fx:0,fxprime:e.slice()},s={x:e.slice(),fx:0,fxprime:e.slice()},l=e.slice(),u=1;a=(n=n||{}).maxIterations||20*e.length,o.fx=t(o.x,o.fxprime),p(i=o.fxprime.slice(),o.fxprime,-1);for(var c=0;ce}),e=0;e0)throw"Initial bisect points must have opposite signs";if(0===o)return e;if(0===s)return n;for(var u=0;u=0&&(e=c),Math.abs(l)=8){var r=function(t,e){var n,i=(e=e||{}).restarts||10,r=[],a={};for(n=0;n=Math.min(e[a].size,e[o].size)?l=1:t.size<=1e-10&&(l=-1),r[a][o]=r[o][a]=l}),{distances:i,constraints:r}}(t,r,a),l=s.distances,h=s.constraints,g=f(l.map(f))/l.length;l=l.map(function(t){return t.map(function(t){return t/g})});var d,v,x=function(t,e){return function(t,e,n,i){var r,a=0;for(r=0;r0&&g<=h||f<0&&g>=h||(a+=2*d*d,e[2*r]+=4*d*(o-u),e[2*r+1]+=4*d*(s-c),e[2*l]+=4*d*(u-o),e[2*l+1]+=4*d*(c-s))}return a}(t,e,l,h)};for(n=0;n=Math.min(l[g].size,l[d].size)&&(p=0),u[g].push({set:d,size:f.size,weight:p}),u[d].push({set:g,size:f.size,weight:p})}var v=[];for(a in u)if(u.hasOwnProperty(a)){var y=0;for(c=0;c0){var r=t[0].x,o=t[0].y;for(i=0;i1){var s,l,u=Math.atan2(t[1].x,t[1].y)-e,c=Math.cos(u),h=Math.sin(u);for(i=0;i2){for(var f=Math.atan2(t[2].x,t[2].y)-e;f<0;)f+=2*Math.PI;for(;f>2*Math.PI;)f-=2*Math.PI;if(f>Math.PI){var p=t[1].y/(1e-10+t[1].x);for(i=0;iu&&p.node().getComputedTextLength()>o&&(h.pop(),p.text(h.join(" ")),h=[c],p=r.append("tspan").text(c),f++)}var g=.35-1.1*f/2,d=r.attr("x"),v=r.attr("y");r.selectAll("tspan").attr("x",d).attr("y",v).attr("dy",function(t,e){return g+1.1*e+"em"})}}function T(t,e,n){var i,r,o=e[0].radius-a(e[0],t);for(i=1;i=u&&(s=r[n],u=c)}var h=d(function(n){return-1*T({x:n[0],y:n[1]},t,e)},[s.x,s.y],{maxIterations:500,minErrorDelta:1e-10}).x,f={x:h[0],y:h[1]},p=!0;for(n=0;nt[n].radius){p=!1;break}for(n=0;n0&&console.log("WARNING: area "+a+" not represented on screen")}return n}function E(t,e,n){var i=[];return i.push("\nM",t,e),i.push("\nm",-n,0),i.push("\na",n,n,0,1,0,2*n,0),i.push("\na",n,n,0,1,0,2*-n,0),i.join(" ")}function D(t){var e=t.split(" ");return{x:parseFloat(e[1]),y:parseFloat(e[2]),radius:-parseFloat(e[4])}}function F(t){var e={};i(t,e);var n=e.arcs;if(0===n.length)return"M 0 0";if(1==n.length){var r=n[0].circle;return E(r.x,r.y,r.radius)}for(var a=["\nM",n[0].p2.x,n[0].p2.y],o=0;ol;a.push("\nA",l,l,0,u?1:0,1,s.p1.x,s.p1.y)}return a.join(" ")}var B=1e-10,R=1e-10;t.intersectionArea=i,t.circleCircleIntersection=s,t.circleOverlap=o,t.circleArea=r,t.distance=a,t.venn=x,t.greedyLayout=b,t.scaleSolution=k,t.normalizeSolution=A,t.bestInitialLayout=_,t.lossFunction=w,t.disjointCluster=M,t.distanceFromIntersectArea=m,t.VennDiagram=function(){function t(t){function f(t){return t.sets in b?b[t.sets]:1==t.sets.length?""+t.sets[0]:void 0}var p=t.datum(),g={};p.forEach(function(t){0==t.size&&1==t.sets.length&&(g[t.sets[0]]=1)});var x={},m={};if((p=p.filter(function(t){return!t.sets.some(function(t){return t in g})})).length>0){var _=v(p,{lossFunction:y});s&&(_=A(_,o,h)),x=k(_,n,i,r),m=L(x,p)}var b={};p.forEach(function(t){t.label&&(b[t.sets]=t.label)}),t.selectAll("svg").data([x]).enter().append("svg");var w=t.select("svg").attr("width",n).attr("height",i),S={},M=!1;w.selectAll(".venn-area path").each(function(t){var n=e.select(this).attr("d");1==t.sets.length&&n&&(M=!0,S[t.sets[0]]=D(n))});var C=function(t){return function(e){return F(t.sets.map(function(t){var r=S[t],a=x[t];return r||(r={x:n/2,y:i/2,radius:1}),a||(a={x:n/2,y:i/2,radius:1}),{x:r.x*(1-e)+a.x*e,y:r.y*(1-e)+a.y*e,radius:r.radius*(1-e)+a.radius*e}}))}},T=w.selectAll(".venn-area").data(p,function(t){return t.sets}),I=T.enter().append("g").attr("class",function(t){return"venn-area venn-"+(1==t.sets.length?"circle":"intersection")}).attr("data-venn-sets",function(t){return t.sets.join("_")}),O=I.append("path"),E=I.append("text").attr("class","label").text(function(t){return f(t)}).attr("text-anchor","middle").attr("dy",".35em").attr("x",n/2).attr("y",i/2);u&&(O.style("fill-opacity","0").filter(function(t){return 1==t.sets.length}).style("fill",function(t){return d(t.sets)}).style("fill-opacity",".25"),E.style("fill",function(t){return 1==t.sets.length?d(t.sets):"#444"}));var B=t;M?(B=t.transition("venn").duration(a)).selectAll("path").attrTween("d",C):B.selectAll("path").attr("d",function(t){return F(t.sets.map(function(t){return x[t]}))});var R=B.selectAll("text").filter(function(t){return t.sets in m}).text(function(t){return f(t)}).attr("x",function(t){return Math.floor(m[t.sets].x)}).attr("y",function(t){return Math.floor(m[t.sets].y)});l&&(M?"on"in R?R.on("end",P(x,f)):R.each("end",P(x,f)):R.each(P(x,f)));var j=T.exit().transition("venn").duration(a).remove();j.selectAll("path").attrTween("d",C);var N=j.selectAll("text").attr("x",n/2).attr("y",i/2);return null!==c&&(E.style("font-size","0px"),R.style("font-size",c),N.style("font-size","0px")),{circles:x,textCentres:m,nodes:T,enter:I,update:B,exit:j}}var n=600,i=350,r=15,a=1e3,o=Math.PI/2,s=!0,l=!0,u=!0,c=null,h=null,f={},p=["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],g=0,d=function(t){if(t in f)return f[t];var e=f[t]=p[g];return(g+=1)>=p.length&&(g=0),e},v=x,y=w;return t.wrap=function(e){return arguments.length?(l=e,t):l},t.width=function(e){return arguments.length?(n=e,t):n},t.height=function(e){return arguments.length?(i=e,t):i},t.padding=function(e){return arguments.length?(r=e,t):r},t.colours=function(e){return arguments.length?(d=e,t):d},t.fontSize=function(e){return arguments.length?(c=e,t):c},t.duration=function(e){return arguments.length?(a=e,t):a},t.layoutFunction=function(e){return arguments.length?(v=e,t):v},t.normalize=function(e){return arguments.length?(s=e,t):s},t.styled=function(e){return arguments.length?(u=e,t):u},t.orientation=function(e){return arguments.length?(o=e,t):o},t.orientationOrder=function(e){return arguments.length?(h=e,t):h},t.lossFunction=function(e){return arguments.length?(y=e,t):y},t},t.wrapText=P,t.computeTextCentres=L,t.computeTextCentre=I,t.sortAreas=function(t,e){function n(t){for(var e=0;e=M&&(M=S+1);!(w=_[M])&&++M=0;)(i=r[a])&&(o&&o!==i.nextSibling&&o.parentNode.insertBefore(i,o),o=i);return this}},function(t,e,n){"use strict";function i(t,e){return te?1:t>=e?0:NaN}var r=n(69);e.a=function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=i);for(var n=this._groups,a=n.length,o=new Array(a),s=0;s1?this.each((null==e?function(t){return function(){delete this[t]}}:"function"==typeof e?function(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}:function(t,e){return function(){this[t]=e}})(t,e)):this.node()[t]}},function(t,e,n){"use strict";function i(t){return t.trim().split(/^|\s+/)}function r(t){return t.classList||new a(t)}function a(t){this._node=t,this._names=i(t.getAttribute("class")||"")}function o(t,e){for(var n=r(t),i=-1,a=e.length;++i=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}},e.a=function(t,e){var n=i(t+"");if(arguments.length<2){for(var a=r(this.node()),l=-1,u=n.length;++l=0&&(n=t.slice(i+1),t=t.slice(0,i)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}})}(t+"",i),o=-1,s=r.length;{if(!(arguments.length<2)){if(null!=e&&"function"!=typeof e)throw new Error("invalid callback: "+e);for(;++o0)for(var n,i,r=new Array(n),a=0;a=0&&(t=t.slice(0,e)),!t||"start"===t})}(e)?i.g:i.h;return function(){var i=o(this,t),s=i.on;s!==r&&(a=(r=s).copy()).on(e,n),i.on=a}}(n,t,e))}},function(t,e,n){"use strict";e.a=function(){return this.on("end.remove",function(t){return function(){var e=this.parentNode;for(var n in this.__transition)if(+n!==t)return;e&&e.removeChild(this)}}(this._id))}},function(t,e,n){"use strict";var i=n(72),r=n(169),a=n(70);e.a=function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=Object(i.selector)(t));for(var o=this._groups,s=o.length,l=new Array(s),u=0;ur.c&&n.name===e)return new i.a([[t]],a,e,+o)}return null}},function(t,e,n){function i(t){var e=s.shape.venn,n=r.mix({},e,t.style);return o.addFillAttrs(n,t),n}var r=n(0),a=n(18),o=n(45),s=n(7),l=r.PathUtil,u=a.registerFactory("venn",{defaultShapeType:"venn",getDefaultPoints:function(t){var e=[];return r.each(t.x,function(n,i){var r=t.y[i];e.push({x:n,y:r})}),e},getActiveCfg:function(t,e){var n=e.lineWidth||1;if("hollow"===t)return{lineWidth:n+1};return{fillOpacity:(e.fillOpacity||e.opacity||1)-.08}},getSelectedCfg:function(t,e){return e&&e.style?e.style:this.getActiveCfg(t,e)}});a.registerShape("venn","venn",{draw:function(t,e){var n=t.origin._origin.path,a=i(t),o=l.parsePathString(n);return e.addShape("path",{attrs:r.mix(a,{path:o})})},getMarkerCfg:function(t){return r.mix({symbol:"circle",radius:4},i(t))}}),a.registerShape("venn","hollow",{draw:function(t,e){var n=t.origin._origin.path,i=function(t){var e=s.shape.hollowVenn,n=r.mix({},e,t.style);return o.addStrokeAttrs(n,t),n}(t),a=l.parsePathString(n);return e.addShape("path",{attrs:r.mix(i,{path:a})})},getMarkerCfg:function(t){return r.mix({symbol:"circle",radius:4},i(t))}}),t.exports=u},function(t,e,n){function i(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}function r(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var a=n(20),o=n(0),s=n(357);n(460);var l=function(t){function e(e){var n;return n=t.call(this,e)||this,o.assign(r(r(n)),s),n}i(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return e.type="violin",e.shapeType="violin",e.generatePoints=!0,e},n.createShapePointsCfg=function(e){var n=t.prototype.createShapePointsCfg.call(this,e);n.size=this.getNormalizedSize(e);var i=this.get("_sizeField");return n._size=e._origin[i],n},n.clearInner=function(){t.prototype.clearInner.call(this),this.set("defaultSize",null)},n._initAttrs=function(){var e=this.get("attrOptions"),n=e.size?e.size.field:this.get("_sizeField")?this.get("_sizeField"):"size";this.set("_sizeField",n),delete e.size,t.prototype._initAttrs.call(this)},e}(a),u=function(t){function e(){return t.apply(this,arguments)||this}i(e,t);return e.prototype.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return e.hasDefaultAdjust=!0,e.adjusts=[{type:"dodge"}],e},e}(l);l.Dodge=u,a.Violin=l,a.ViolinDodge=u,t.exports=l},function(t,e,n){function i(t){var e=c.shape.venn,n=s.mix({},e,t.style);return u.addFillAttrs(n,t),t.color&&(n.stroke=n.stroke||t.color),n}function r(t){var e=c.shape.hollowVenn,n=s.mix({},e,t.style);return u.addStrokeAttrs(n,t),n}function a(t){for(var e=[],n=0;n=0;r--)for(var a=e.getFacetsByLevel(t,r),o=0;op.x||a.yf.y)return}s.style.cursor="crosshair",e.startPoint=a,e.brushShape=null,e.brushing=!0,c?c.clear():(c=n.addGroup({zIndex:5})).initTransform(),e.container=c,"POLYGON"===i&&(e.polygonPath="M "+a.x+" "+a.y)}}}},n.process=function(t){var e=this,n=e.brushing,i=e.dragging,a=e.type,o=e.plot,s=e.startPoint,l=e.xScale,u=e.yScale,c=e.canvas;if(n||i){var h={x:t.offsetX,y:t.offsetY},f=c.get("canvasDOM");if(n){f.style.cursor="crosshair";var p=o.start,g=o.end,d=e.polygonPath,v=e.brushShape,y=e.container;e.plot&&e.inPlot&&(h=e._limitCoordScope(h));var x,m,_,b;"Y"===a?(x=p.x,m=h.y>=s.y?s.y:h.y,_=Math.abs(p.x-g.x),b=Math.abs(s.y-h.y)):"X"===a?(x=h.x>=s.x?s.x:h.x,m=g.y,_=Math.abs(s.x-h.x),b=Math.abs(g.y-p.y)):"XY"===a?(h.x>=s.x?(x=s.x,m=h.y>=s.y?s.y:h.y):(x=h.x,m=h.y>=s.y?s.y:h.y),_=Math.abs(s.x-h.x),b=Math.abs(s.y-h.y)):"POLYGON"===a&&(d+="L "+h.x+" "+h.y,e.polygonPath=d,v?!v.get("destroyed")&&v.attr(r.mix({},v._attrs,{path:d})):v=y.addShape("path",{attrs:r.mix(e.style,{path:d})})),"POLYGON"!==a&&(v?!v.get("destroyed")&&v.attr(r.mix({},v._attrs,{x:x,y:m,width:_,height:b})):v=y.addShape("rect",{attrs:r.mix(e.style,{x:x,y:m,width:_,height:b})})),e.brushShape=v}else if(i){f.style.cursor="move";var w=e.selection;if(w&&!w.get("destroyed"))if("POLYGON"===a){var S=e.prePoint;e.selection.translate(h.x-S.x,h.y-S.y)}else e.dragoffX&&w.attr("x",h.x-e.dragoffX),e.dragoffY&&w.attr("y",h.y-e.dragoffY)}e.prePoint=h,c.draw();var M=e._getSelected(),C=M.data,A=M.shapes,k=M.xValues,P=M.yValues,T={data:C,shapes:A,x:h.x,y:h.y};l&&(T[l.field]=k),u&&(T[u.field]=P),e.onDragmove&&e.onDragmove(T),e.onBrushmove&&e.onBrushmove(T)}},n.end=function(t){var e=this,n=e.data,i=e.shapes,a=e.xValues,o=e.yValues,s=e.canvas,l=e.type,u=e.startPoint,c=e.chart,h=e.container,f=e.xScale,p=e.yScale,g=t.offsetX,d=t.offsetY;if(s.get("canvasDOM").style.cursor="default",Math.abs(u.x-g)<=1&&Math.abs(u.y-d)<=1)return e.brushing=!1,void(e.dragging=!1);var v={data:n,shapes:i,x:g,y:d};if(f&&(v[f.field]=a),p&&(v[p.field]=o),e.dragging)e.dragging=!1,e.onDragend&&e.onDragend(v);else if(e.brushing){e.brushing=!1;var y=e.brushShape,x=e.polygonPath;"POLYGON"===l&&(x+="z",y&&!y.get("destroyed")&&y.attr(r.mix({},y._attrs,{path:x})),e.polygonPath=x,s.draw()),e.onBrushend?e.onBrushend(v):c&&e.filter&&(h.clear(),"X"===l?f&&c.filter(f.field,function(t){return a.indexOf(t)>-1}):"Y"===l?p&&c.filter(p.field,function(t){return o.indexOf(t)>-1}):(f&&c.filter(f.field,function(t){return a.indexOf(t)>-1}),p&&c.filter(p.field,function(t){return o.indexOf(t)>-1})),c.repaint())}},n.reset=function(){var t=this.chart,e=this.filter,n=this.brushShape,i=this.canvas;t&&e&&(t.get("options").filters={},t.repaint()),n&&(n.destroy(),i.draw())},n._limitCoordScope=function(t){var e=this.plot,n=e.start,i=e.end;return t.xi.x&&(t.x=i.x),t.yn.y&&(t.y=n.y),t},n._getSelected=function(){var t=this,e=t.chart,n=t.xScale,i=t.yScale,r=t.brushShape,a=t.canvas,o=a.get("pixelRatio"),s=[],l=[],u=[],c=[];if(e){e.get("geoms").map(function(t){return t.getShapes().map(function(t){var e=t.get("origin");return Array.isArray(e)||(e=[e]),e.map(function(e){if(r.isHit(e.x*o,e.y*o)){s.push(t);var a=e._origin;c.push(a),n&&l.push(a[n.field]),i&&u.push(a[i.field])}return e}),t}),t})}return t.shapes=s,t.xValues=l,t.yValues=u,t.data=c,a.draw(),{data:c,xValues:l,yValues:u,shapes:s}},e}(n(171));t.exports=s},function(t,e,n){function i(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var r=n(0),a=n(171),o=n(469),s=n(377),l=n(378),u=["X","Y","XY"],c="X",h=function(t){function e(e,n){var a,s=i(i(a=t.call(this,e,n)||this));s.type=s.type.toUpperCase(),s.chart=n,s.coord=n.get("coord");var h=s.data=n.get("data");o(n);var f=n.getYScales(),p=n.getXScale();f.push(p);var g=n.get("scaleController");return f.forEach(function(t){var e=t.field;s.limitRange[e]=l(h,t);var n=g.defs[e]||{};s.originScaleDefsByField[e]=r.mix(n,{nice:!!n.nice}),t.isLinear&&(s.stepByField[e]=(t.max-t.min)*s.stepRatio)}),-1===u.indexOf(s.type)&&(s.type=c),s._disableTooltip(),a}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return r.mix({},e,{type:c,stepRatio:.05,limitRange:{},stepByField:{},threshold:20,originScaleDefsByField:{},previousPoint:null,isDragging:!1})},n._disableTooltip=function(){var t=this.chart;t.get("tooltipController")&&(this._showTooltip=!0,t.tooltip(!1))},n._enableTooltip=function(t){var e=this.chart;this._showTooltip&&(e.tooltip(!0),e.showTooltip(t))},n._applyTranslate=function(t,e,n){void 0===e&&(e=0);t.isLinear?this._translateLinearScale(t,e,n):this._translateCatScale(t,e,n)},n._translateCatScale=function(t,e,n){var i=this.chart,a=t.type,o=t.field,l=t.values,u=t.ticks,c=s(i,o),h=this.limitRange[o],f=e/n,p=l.length,g=Math.max(1,Math.abs(parseInt(f*p))),d=h.indexOf(l[0]),v=h.indexOf(l[p-1]);if(e>0&&d>=0){for(var y=0;y0;y++)d-=1,v-=1;var x=h.slice(d,v+1),m=null;if("timeCat"===a){for(var _=u.length>2?u[1]-u[0]:864e5,b=u[0]-_;b>=x[0];b-=_)u.unshift(b);m=u}i.scale(o,r.mix({},c,{values:x,ticks:m}))}else if(e<0&&v<=h.length-1){for(var w=0;w2?u[1]-u[0]:864e5,A=u[u.length-1]+C;A<=S[S.length-1];A+=C)u.push(A);M=u}i.scale(o,r.mix({},c,{values:S,ticks:M}))}},n._translateLinearScale=function(t,e,n){var i=this.chart,a=this.limitRange,o=t.min,l=t.max,u=t.field;if(o!==a[u].min||l!==a[u].max){var c=e/n,h=l-o,f=s(i,u);i.scale(u,r.mix({},f,{nice:!1,min:o+c*h,max:l+c*h}))}},n.start=function(t){this.canvas.get("canvasDOM").style.cursor="pointer",this.isDragging=!0,this.previousPoint={x:t.x,y:t.y},this._disableTooltip()},n.process=function(t){var e=this;if(e.isDragging){var n=e.chart,i=e.type,r=e.canvas,a=e.coord,o=e.threshold;r.get("canvasDOM").style.cursor="move";var s=e.previousPoint,l=t,u=l.x-s.x,c=l.y-s.y,h=!1;if(Math.abs(u)>o&&i.indexOf("X")>-1){h=!0;var f=n.getXScale();e._applyTranslate(f,f.isLinear?-u:u,a.width)}if(Math.abs(c)>o&&i.indexOf("Y")>-1){h=!0;n.getYScales().forEach(function(t){e._applyTranslate(t,l.y-s.y,a.height)})}h&&(e.previousPoint=l,n.repaint())}},n.end=function(t){this.isDragging=!1;this.canvas.get("canvasDOM").style.cursor="default",this._enableTooltip(t)},n.reset=function(){var t=this.view,e=this.originScaleDefsByField,n=t.getYScales(),i=t.getXScale();n.push(i),n.forEach(function(n){if(n.isLinear){var i=n.field;t.scale(i,e[i])}}),t.repaint(),this._disableTooltip()},e}(a);t.exports=h},function(t,e,n){var i=n(0),r=n(71),a=n(376);t.exports=function(t){t.on("beforeinitgeoms",function(){t.set("limitInPlot",!0);var e=t.get("data"),n=a(t);if(!n)return e;var o=t.get("geoms"),s=!1;i.each(o,function(t){if(-1!==["area","line","path"].indexOf(t.get("type")))return s=!0,!1});var l=[];if(i.each(n,function(t,e){!s&&t&&(t.values||t.min||t.max)&&l.push(e)}),0===l.length)return e;var u=[];i.each(e,function(t){var e=!0;i.each(l,function(a){var o=t[a];if(o){var s=n[a];if("timeCat"===s.type){var l=s.values;i.isNumber(l[0])&&(o=r.toTimeStamp(o))}(s.values&&-1===s.values.indexOf(o)||s.min&&os.max)&&(e=!1)}}),e&&u.push(t)}),t.set("filteredData",u)})}},function(t,e,n){var i=n(0),r=n(171),a=n(471),o=n(378),s=function(t){function e(e,n){var r,a=(r=t.call(this,e,n)||this).getDefaultCfg();return n.set("_scrollBarCfg",i.deepMix({},a,e)),n.set("_limitRange",{}),n.get("_horizontalBar")||n.get("_verticalBar")||r._renderScrollBars(),r}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return i.mix({},e,{startEvent:null,processEvent:null,endEvent:null,resetEvent:null,type:"X",xStyle:{backgroundColor:"rgba(202, 215, 239, .2)",fillerColor:"rgba(202, 215, 239, .75)",size:4,lineCap:"round",offsetX:0,offsetY:-10},yStyle:{backgroundColor:"rgba(202, 215, 239, .2)",fillerColor:"rgba(202, 215, 239, .75)",size:4,lineCap:"round",offsetX:8,offsetY:0}})},n._renderScrollBars=function(){var t=this.chart,e=t.get("_scrollBarCfg");if(e){var n=t.get("data"),i=t.get("plotRange");i.width=Math.abs(i.br.x-i.bl.x),i.height=Math.abs(i.tl.y-i.bl.y);var r=t.get("backPlot"),s=t.get("canvas").get("height"),l=t.get("_limitRange"),u=e.type;if(u.indexOf("X")>-1){var c=e.xStyle,h=c.offsetX,f=c.offsetY,p=c.lineCap,g=c.backgroundColor,d=c.fillerColor,v=c.size,y=t.getXScale(),x=l[y.field];x||(x=o(n,y),l[y.field]=x);var m=a(y,x,y.type),_=t.get("_horizontalBar"),b=s-v/2+f;if(_){_.get("children")[1].attr({x1:Math.max(i.bl.x+i.width*m[0]+h,i.bl.x),x2:Math.min(i.bl.x+i.width*m[1]+h,i.br.x)})}else(_=r.addGroup({className:"horizontalBar"})).addShape("line",{attrs:{x1:i.bl.x+h,y1:b,x2:i.br.x+h,y2:b,lineWidth:v,stroke:g,lineCap:p}}),_.addShape("line",{attrs:{x1:Math.max(i.bl.x+i.width*m[0]+h,i.bl.x),y1:b,x2:Math.min(i.bl.x+i.width*m[1]+h,i.br.x),y2:b,lineWidth:v,stroke:d,lineCap:p}}),t.set("_horizontalBar",_)}if(u.indexOf("Y")>-1){var w=e.yStyle,S=w.offsetX,M=w.offsetY,C=w.lineCap,A=w.backgroundColor,k=w.fillerColor,P=w.size,T=t.getYScales()[0],I=l[T.field];I||(I=o(n,T),l[T.field]=I);var O=a(T,I,T.type),L=t.get("_verticalBar"),E=P/2+S;if(L){L.get("children")[1].attr({y1:Math.max(i.tl.y+i.height*O[0]+M,i.tl.y),y2:Math.min(i.tl.y+i.height*O[1]+M,i.bl.y)})}else(L=r.addGroup({className:"verticalBar"})).addShape("line",{attrs:{x1:E,y1:i.tl.y+M,x2:E,y2:i.bl.y+M,lineWidth:P,stroke:A,lineCap:C}}),L.addShape("line",{attrs:{x1:E,y1:Math.max(i.tl.y+i.height*O[0]+M,i.tl.y),x2:E,y2:Math.min(i.tl.y+i.height*O[1]+M,i.bl.y),lineWidth:P,stroke:k,lineCap:C}}),t.set("_verticalBar",L)}}},n._clear=function(){var t=this.chart;if(t){var e=t.get("_horizontalBar"),n=t.get("_verticalBar");e&&e.remove(!0),n&&n.remove(!0),t.set("_horizontalBar",null),t.set("_verticalBar",null)}},n._bindEvents=function(){this._onAfterclearOrBeforechangedata=this._onAfterclearOrBeforechangedata.bind(this),this._onAfterclearinner=this._onAfterclearinner.bind(this),this._onAfterdrawgeoms=this._onAfterdrawgeoms.bind(this);var t=this.chart;t.on("afterclear",this._onAfterclearOrBeforechangedata),t.on("beforechangedata",this._onAfterclearOrBeforechangedata),t.on("afterclearinner",this._onAfterclearinner),t.on("afterdrawgeoms",this._onAfterdrawgeoms)},n._onAfterclearOrBeforechangedata=function(){this.chart&&this.chart.set("_limitRange",{})},n._onAfterclearinner=function(){this._clear()},n._onAfterdrawgeoms=function(){this._renderScrollBars()},n._clearEvents=function(){var t=this.chart;t&&(t.off("afterclear",this._onAfterclearOrBeforechangedata),t.off("beforechangedata",this._onAfterclearOrBeforechangedata),t.off("afterclearinner",this._onAfterclearinner),t.off("afterdrawgeoms",this._onAfterdrawgeoms))},n.destroy=function(){this._clearEvents(),this._clear(),this.canvas.draw()},e}(r);t.exports=s},function(t,e){t.exports=function(t,e,n){if(!t)return[0,1];var i=0,r=0;if("linear"===n){var a=e.min,o=e.max-a;i=(t.min-a)/o,r=(t.max-a)/o}else{var s=e,l=t.values,u=s.indexOf(l[0]),c=s.indexOf(l[l.length-1]);i=u/(s.length-1),r=c/(s.length-1)}return[i,r]}},function(t,e,n){function i(t,e){var n={};for(var i in e)n[i]=t[i];return n}var r=n(0),a=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return r.mix({},e,{startEvent:"mouseup",processEvent:null,selectStyle:{fillOpacity:1},unSelectStyle:{fillOpacity:.1},cancelable:!0})},n.start=function(t){var e,n=[];if(this.view.eachShape(function(i,r){r.isPointInPath(t.x,t.y)?e=r:n.push(r)}),e)if(e.get("_selected")){if(!this.cancelable)return;this.reset()}else{var a=this.selectStyle,o=this.unSelectStyle,s=i(e.attr(),e);e.set("_originAttrs",s),e.attr(a),r.each(n,function(t){var e=t.get("_originAttrs");e&&t.attr(e),t.set("_selected",!1),o&&(e=i(t.attr(),o),t.set("_originAttrs",e),t.attr(o))}),e.set("_selected",!0),this.selectedShape=e,this.canvas.draw()}else this.reset()},n.end=function(t){var e=this.selectedShape;e&&!e.get("destroyed")&&e.get("origin")&&(t.data=e.get("origin")._origin,t.shapeInfo=e.get("origin"),t.shape=e,t.selected=!!e.get("_selected"))},n.reset=function(){if(this.selectedShape){var t=this.view.get("geoms")[0].get("container").get("children")[0].get("children");r.each(t,function(t){var e=t.get("_originAttrs");e&&(t._attrs=e,t.set("_originAttrs",null)),t.set("_selected",!1)}),this.canvas.draw()}},e}(n(171));t.exports=a},function(t,e,n){function i(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var r=n(474),a=n(147),o=n(0),s=n(16),l=n(7),u=n(171),c=n(377),h=n(376),f=s.Canvas,p=o.DomUtil,g=o.isNumber,d=function(t){function e(e,n){var r,a=i(i(r=t.call(this,e,n)||this));return a._initContainer(),a._initStyle(),a.render(),r}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return o.mix({},e,{startEvent:null,processEvent:null,endEvent:null,resetEvent:null,height:26,width:"auto",padding:l.plotCfg.padding,container:null,xAxis:null,yAxis:null,fillerStyle:{fill:"#BDCCED",fillOpacity:.3},backgroundStyle:{stroke:"#CCD6EC",fill:"#CCD6EC",fillOpacity:.3,lineWidth:1},range:[0,100],layout:"horizontal",textStyle:{fill:"#545454"},handleStyle:{img:"https://gw.alipayobjects.com/zos/rmsportal/QXtfhORGlDuRvLXFzpsQ.png",width:5},backgroundChart:{type:["area"],color:"#CCD6EC"}})},n._initContainer=function(){var t=this.container;if(!t)throw new Error("Please specify the container for the Slider!");o.isString(t)?this.domContainer=document.getElementById(t):this.domContainer=t},n.forceFit=function(){var t=this;if(t&&!t.destroyed){var e=p.getWidth(t.domContainer),n=t.height;if(e!==t.domWidth){var i=t.canvas;i.changeSize(e,n),t.bgChart&&t.bgChart.changeWidth(e),i.clear(),t._initWidth(),t._initSlider(),t._bindEvent(),i.draw()}}},n._initForceFitEvent=function(){var t=setTimeout(o.wrapBehavior(this,"forceFit"),200);clearTimeout(this.resizeTimer),this.resizeTimer=t},n._initStyle=function(){var t=this;t.handleStyle=o.mix({width:t.height,height:t.height},t.handleStyle),"auto"===t.width&&window.addEventListener("resize",o.wrapBehavior(t,"_initForceFitEvent"))},n._initWidth=function(){var t,e=this;t="auto"===e.width?p.getWidth(e.domContainer):e.width,e.domWidth=t;var n=o.toAllPadding(e.padding);"horizontal"===e.layout?(e.plotWidth=t-n[1]-n[3],e.plotPadding=n[3],e.plotHeight=e.height):"vertical"===e.layout&&(e.plotWidth=e.width,e.plotHeight=e.height-n[0]-n[2],e.plotPadding=n[0])},n._initCanvas=function(){var t=this.domWidth,e=this.height,n=new f({width:t,height:e,containerDOM:this.domContainer,capture:!1}),i=n.get("el");i.style.position="absolute",i.style.top=0,i.style.left=0,i.style.zIndex=3,this.canvas=n},n._initBackground=function(){var t,e=this,n=this.chart,i=n.getAllGeoms[0],r=e.data=e.data||n.get("data"),s=n.getXScale(),l=e.xAxis||s.field,u=e.yAxis||n.getYScales()[0].field,c=o.deepMix((t={},t[""+l]={range:[0,1]},t),h(n),e.scales);if(delete c[l].min,delete c[l].max,!r)throw new Error("Please specify the data!");if(!l)throw new Error("Please specify the xAxis!");if(!u)throw new Error("Please specify the yAxis!");var f=e.backgroundChart,p=f.type||i.get("type"),g=f.color||"grey";o.isArray(p)||(p=[p]);var d=o.toAllPadding(e.padding),v=new a({container:e.container,width:e.domWidth,height:e.height,padding:[0,d[1],0,d[3]],animate:!1});v.source(r),v.scale(c),v.axis(!1),v.tooltip(!1),v.legend(!1),o.each(p,function(t){v[t]().position(l+"*"+u).color(g).opacity(1)}),v.render(),e.bgChart=v,e.scale="horizontal"===e.layout?v.getXScale():v.getYScales()[0],"vertical"===e.layout&&v.destroy()},n._initRange=function(){var t=this,e=t.startRadio,n=t.endRadio,i=t._startValue,r=t._endValue,a=t.scale,o=0,s=1;g(e)?o=e:i&&(o=a.scale(a.translate(i))),g(n)?s=n:r&&(s=a.scale(a.translate(r)));var l=t.minSpan,u=t.maxSpan,c=0;if("time"===a.type||"timeCat"===a.type){var h=a.values,f=h[0];c=h[h.length-1]-f}else a.isLinear&&(c=a.max-a.min);c&&l&&(t.minRange=l/c*100),c&&u&&(t.maxRange=u/c*100);var p=[100*o,100*s];return t.range=p,p},n._getHandleValue=function(t){var e=this,n=e.range,i=n[0]/100,r=n[1]/100,a=e.scale;return"min"===t?e._startValue?e._startValue:a.invert(i):e._endValue?e._endValue:a.invert(r)},n._initSlider=function(){var t=this,e=t.canvas,n=t._initRange(),i=t.scale,a=e.addGroup(r,{middleAttr:t.fillerStyle,range:n,minRange:t.minRange,maxRange:t.maxRange,layout:t.layout,width:t.plotWidth,height:t.plotHeight,backgroundStyle:t.backgroundStyle,textStyle:t.textStyle,handleStyle:t.handleStyle,minText:i.getText(t._getHandleValue("min")),maxText:i.getText(t._getHandleValue("max"))});"horizontal"===t.layout?a.translate(t.plotPadding,0):"vertical"===t.layout&&a.translate(0,t.plotPadding),t.rangeElement=a},n._updateElement=function(t,e){var n=this,i=n.chart,r=n.scale,a=n.rangeElement,s=r.field,l=a.get("minTextElement"),u=a.get("maxTextElement"),h=r.invert(t),f=r.invert(e),p=r.getText(h),g=r.getText(f);l.attr("text",p),u.attr("text",g),n._startValue=p,n._endValue=g,n.onChange&&n.onChange({startText:p,endText:g,startValue:h,endValue:f,startRadio:t,endRadio:e}),i.scale(s,o.mix({},c(i,s),{nice:!1,min:h,max:f})),i.repaint()},n._bindEvent=function(){var t=this;t.rangeElement.on("sliderchange",function(e){var n=e.range,i=n[0]/100,r=n[1]/100;t._updateElement(i,r)})},n.clear=function(){var t=this;t.canvas.clear(),t.bgChart&&t.bgChart.destroy(),t.bgChart=null,t.scale=null,t.canvas.draw()},n.repaint=function(){this.clear(),this.render()},n.render=function(){var t=this;t._initWidth(),t._initCanvas(),t._initBackground(),t._initSlider(),t._bindEvent(),t.canvas.draw()},n.destroy=function(){var t=this;clearTimeout(t.resizeTimer);t.rangeElement.off("sliderchange"),t.bgChart&&t.bgChart.destroy(),t.canvas.destroy();for(var e=t.domContainer;e.hasChildNodes();)e.removeChild(e.firstChild);window.removeEventListener("resize",o.getWrapBehavior(t,"_initForceFitEvent")),t.destroyed=!0},e}(u);t.exports=d},function(t,e,n){var i=n(0),r=n(16).Group,a=i.DomUtil,o=function(t){function e(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){return{range:null,middleAttr:null,backgroundElement:null,minHandleElement:null,maxHandleElement:null,middleHandleElement:null,currentTarget:null,layout:"vertical",width:null,height:null,pageX:null,pageY:null}},n._initHandle=function(t){var e,n,r,a=this.addGroup(),o=this.get("layout"),s=this.get("handleStyle"),l=s.img,u=s.width,c=s.height;if("horizontal"===o){var h=s.width;r="ew-resize",n=a.addShape("Image",{attrs:{x:-h/2,y:0,width:h,height:c,img:l,cursor:r}}),e=a.addShape("Text",{attrs:i.mix({x:"min"===t?-(h/2+5):h/2+5,y:c/2,textAlign:"min"===t?"end":"start",textBaseline:"middle",text:"min"===t?this.get("minText"):this.get("maxText"),cursor:r},this.get("textStyle"))})}else r="ns-resize",n=a.addShape("Image",{attrs:{x:0,y:-c/2,width:u,height:c,img:l,cursor:r}}),e=a.addShape("Text",{attrs:i.mix({x:u/2,y:"min"===t?c/2+5:-(c/2+5),textAlign:"center",textBaseline:"middle",text:"min"===t?this.get("minText"):this.get("maxText"),cursor:r},this.get("textStyle"))});return this.set(t+"TextElement",e),this.set(t+"IconElement",n),a},n._initSliderBackground=function(){var t=this.addGroup();return t.initTransform(),t.translate(0,0),t.addShape("Rect",{attrs:i.mix({x:0,y:0,width:this.get("width"),height:this.get("height")},this.get("backgroundStyle"))}),t},n._beforeRenderUI=function(){var t=this._initSliderBackground(),e=this._initHandle("min"),n=this._initHandle("max"),i=this.addShape("rect",{attrs:this.get("middleAttr")});this.set("middleHandleElement",i),this.set("minHandleElement",e),this.set("maxHandleElement",n),this.set("backgroundElement",t),t.set("zIndex",0),i.set("zIndex",1),e.set("zIndex",2),n.set("zIndex",2),i.attr("cursor","move"),this.sort()},n._renderUI=function(){"horizontal"===this.get("layout")?this._renderHorizontal():this._renderVertical()},n._transform=function(t){var e=this.get("range"),n=e[0]/100,i=e[1]/100,r=this.get("width"),a=this.get("height"),o=this.get("minHandleElement"),s=this.get("maxHandleElement"),l=this.get("middleHandleElement");o.resetMatrix?(o.resetMatrix(),s.resetMatrix()):(o.initTransform(),s.initTransform()),"horizontal"===t?(l.attr({x:r*n,y:0,width:(i-n)*r,height:a}),o.translate(n*r,0),s.translate(i*r,0)):(l.attr({x:0,y:a*(1-i),width:r,height:(i-n)*a}),o.translate(0,(1-n)*a),s.translate(0,(1-i)*a))},n._renderHorizontal=function(){this._transform("horizontal")},n._renderVertical=function(){this._transform("vertical")},n._bindUI=function(){this.on("mousedown",i.wrapBehavior(this,"_onMouseDown"))},n._isElement=function(t,e){var n=this.get(e);if(t===n)return!0;if(n.isGroup){return n.get("children").indexOf(t)>-1}return!1},n._getRange=function(t,e){var n=t+e;return n=n>100?100:n,n=n<0?0:n},n._limitRange=function(t,e,n){n[0]=this._getRange(t,n[0]),n[1]=n[0]+e,n[1]>100&&(n[1]=100,n[0]=n[1]-e)},n._updateStatus=function(t,e){var n="x"===t?this.get("width"):this.get("height");t=i.upperFirst(t);var r,a=this.get("range"),o=this.get("page"+t),s=this.get("currentTarget"),l=this.get("rangeStash"),u="vertical"===this.get("layout")?-1:1,c=e["page"+t],h=(c-o)/n*100*u,f=this.get("minRange"),p=this.get("maxRange");a[1]<=a[0]?(this._isElement(s,"minHandleElement")||this._isElement(s,"maxHandleElement"))&&(a[0]=this._getRange(h,a[0]),a[1]=this._getRange(h,a[0])):(this._isElement(s,"minHandleElement")&&(a[0]=this._getRange(h,a[0]),f&&a[1]-a[0]<=f&&this._limitRange(h,f,a),p&&a[1]-a[0]>=p&&this._limitRange(h,p,a)),this._isElement(s,"maxHandleElement")&&(a[1]=this._getRange(h,a[1]),f&&a[1]-a[0]<=f&&this._limitRange(h,f,a),p&&a[1]-a[0]>=p&&this._limitRange(h,p,a))),this._isElement(s,"middleHandleElement")&&(r=l[1]-l[0],this._limitRange(h,r,a)),this.emit("sliderchange",{range:a}),this.set("page"+t,c),this._renderUI(),this.get("canvas").draw()},n._onMouseDown=function(t){var e=t.currentTarget,n=t.event,i=this.get("range");n.stopPropagation(),n.preventDefault(),this.set("pageX",n.pageX),this.set("pageY",n.pageY),this.set("currentTarget",e),this.set("rangeStash",[i[0],i[1]]),this._bindCanvasEvents()},n._bindCanvasEvents=function(){var t=this.get("canvas").get("containerDOM");this.onMouseMoveListener=a.addEventListener(t,"mousemove",i.wrapBehavior(this,"_onCanvasMouseMove")),this.onMouseUpListener=a.addEventListener(t,"mouseup",i.wrapBehavior(this,"_onCanvasMouseUp")),this.onMouseLeaveListener=a.addEventListener(t,"mouseleave",i.wrapBehavior(this,"_onCanvasMouseUp"))},n._onCanvasMouseMove=function(t){"horizontal"===this.get("layout")?this._updateStatus("x",t):this._updateStatus("y",t)},n._onCanvasMouseUp=function(){this._removeDocumentEvents()},n._removeDocumentEvents=function(){this.onMouseMoveListener.remove(),this.onMouseUpListener.remove(),this.onMouseLeaveListener.remove()},e}(r);t.exports=o},function(t,e,n){function i(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var r=n(0),a=n(171),o=n(377),s=n(378),l=["X","Y","XY"],u="X",c=function(t){function e(e,n){var a,o=i(i(a=t.call(this,e,n)||this));o.chart=n,o.type=o.type.toUpperCase();var c=o.data=n.get("data"),h=n.getYScales(),f=n.getXScale();h.push(f);var p=n.get("scaleController");return h.forEach(function(t){var e=t.field,n=p.defs[e]||{};o.limitRange[e]=s(c,t),o.originScaleDefsByField[e]=r.mix(n,{nice:!!n.nice}),t.isLinear?o.stepByField[e]=(t.max-t.min)*o.stepRatio:o.stepByField[e]=o.catStep}),-1===l.indexOf(o.type)&&(o.type=u),a}!function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}(e,t);var n=e.prototype;return n.getDefaultCfg=function(){var e=t.prototype.getDefaultCfg.call(this);return r.mix({},e,{processEvent:"mousewheel",type:u,stepRatio:.05,stepByField:{},minScale:1,maxScale:4,catStep:2,limitRange:{},originScaleDefsByField:{}})},n._applyScale=function(t,e,n,i){void 0===n&&(n=0);var a=this,s=a.chart,l=a.stepByField;if(t.isLinear){var u=t.min,c=t.max,h=t.field,f=1-n,p=l[h]*e,g=u+p*n,d=c-p*f;d>g&&s.scale(h,{nice:!1,min:g,max:d})}else{var v=t.field,y=t.values,x=a.chart,m=x.get("coord"),_=o(x,v),b=a.limitRange[v],w=b.length,S=w/a.maxScale,M=w/a.minScale,C=y.length,A=m.invertPoint(i).x,k=C-e*this.catStep,P=parseInt(k*A),T=k+P;if(e>0&&C>=S){var I=P,O=T;T>C&&(O=C-1,I=C-k);var L=y.slice(I,O);x.scale(v,r.mix({},_,{values:L}))}else if(e<0&&C<=M){var E=b.indexOf(y[0]),D=b.indexOf(y[C-1]),F=Math.max(0,E-P),B=Math.min(D+T,w),R=b.slice(F,B);x.scale(v,r.mix({},_,{values:R}))}}},n.process=function(t){var e=this,n=e.chart,i=e.type,r=n.get("coord"),a=t.deltaY,o=r.invertPoint(t);if(a){e.onZoom&&e.onZoom(a,o,e),a>0?e.onZoomin&&e.onZoomin(a,o,e):e.onZoomout&&e.onZoomout(a,o,e);var s=a/Math.abs(a);if(i.indexOf("X")>-1&&e._applyScale(n.getXScale(),s,o.x,t),i.indexOf("Y")>-1){n.getYScales().forEach(function(n){e._applyScale(n,s,o.y,t)})}}n.repaint()},n.reset=function(){var t=this.view,e=this.originScaleDefsByField,n=t.getYScales(),i=t.getXScale();n.push(i),n.forEach(function(n){if(n.isLinear){var i=n.field;t.scale(i,e[i])}}),t.repaint()},e}(a);t.exports=c}])}); \ No newline at end of file diff --git a/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/jquery.min.js b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/jquery.min.js new file mode 100644 index 0000000..fad9ab1 --- /dev/null +++ b/chushang-visual/chushang-sentinel/src/main/webapp/resources/lib/js/jquery.min.js @@ -0,0 +1,5 @@ +/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){ +return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*\s*$/g,ia={option:[1,""],thead:[1,"","
        "],col:[2,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n("